どうも、ぽっぽです。
今まで学んだ知識をもとに、より実践的なサンプルアプリケーションを作成してより深くReduxのことを学んでいきましょう。
小さなソーシャルメディアアプリを構築します。アプリケーションを構築することでReduxをどうやって使うか理解していきましょう。
Project Setup
公式が用意ししてくれるているGithub repoからローカルにクローンしましょう。npm install
を実行し、npm start
をタイプしてプロジェクトをスタートしましょう。
このプロジェクトの最終形態を見たい場合は、tutorial-steps
branchを確認しましょう。
初期プロジェクトの中身はこのようになっています。
/src
index.js
:アプリケーションのエントリーポイントファイルです。React-Reduxの<Provider>
コンポーネントとメインの<App>
コンポーネントをレンダリングします。App.js
:メインのアプリケーションコンポーネント。トップのナビバーとその他の中身をクライアントサイドルーチンを操作して描画します。index.css
:完全なアプリケーションのスタイルを定義します。/api
client.js
:小さなAJAXリクエストのクライアント、GETとPOSTリクエストを送ることが出来ます。server.js
:取得するデータのフェイクREST APIを提供。あとからフェイクエンドポイントからデータを取得します。
/app
Navbar.js
:トップのヘッダーとナビコンテントを描画します。store.js
:Redux storeインスタンスを作成します。
Main Posts Feed
ソーシャルメディアアプリの主な特徴は投稿のリストさせることです。チュートリアルをすすめるにつれていくつかの仕様を追加します。最初のゴールは投稿の一覧を表示させることだけです。
Creating the Posts Slice
最初のステップは、投稿データを含む”slice”を作ることです。Redux Storeにデータがあると、ページにデータを表示するためのReactコンポーネントを作ることが出来ます。
投稿したデータを操作するreducer関数を作るためにRedux ToolkitのcreateSlice
関数を使いましょう。Reducer関数は初期データを持つことが出来ます、アプリが開始されたらRedux storeにそれらの値がロードされます。
features/posts/postsSlice.js
import { createSlice } from '@reduxjs/toolkit'
const initialState = [
{ id: '1', title: 'First Post!', content: 'Hello!' },
{ id: '2', title: 'Second Post', content: 'More text' }
]
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {}
})
export default postsSlice.reducer
次にRedux storeに、作成したReducer関数をインポートしましょう。
app/store.js
import { configureStore } from '@reduxjs/toolkit'
import postsReducer from '../features/posts/postsSlice'
export default configureStore({
reducer: {
posts: postsReducer
}
})
これでReduxに、トップレベルstateオブジェクトがposts
という名前のフィールドを持ち、state.posts
用のすべてのデータがpostsReducer
関数によってactionsがdispatchされたときに更新されます。
Showing the Posts List
storeに投稿データがあるので、投稿一覧を表示するReactコンポーネントを作りましょう。投稿に関するものはposts
フォルダに置きましょう。
ReactコンポーネントがRedux storeからデータを読み出すにはReact-ReduxライブラリのuseSelector
フックを使います。”selector functions”はパラメータとして完全なRedux state
オブジェクトを渡し、storeからコンポーネントで必要なデータを返します。
Redux storeからstate.posts
を読み取り、投稿の配列からデータを取り出してスクリーンに表示しましょう。
features/posts/PostsList.js
import React from 'react'
import { useSelector } from 'react-redux'
export const PostsList = () => {
const posts = useSelector(state => state.posts)
const renderedPosts = posts.map(post => (
<article className="post-excerpt" key={post.id}>
<h3>{post.title}</h3>
<p className="post-content">{post.content.substring(0, 100)}</p>
</article>
))
return (
<section className="posts-list">
<h2>Posts</h2>
{renderedPosts}
</section>
)
}
ウェルカムメッセージの代わりにPostList
コンポーネントを表示させるため、App.js
のルーティングを更新しましょう。PostList
をインポートしてウエルカムテキストを<PostsList />
に変えましょう。
App.js
import React from 'react'
import {
BrowserRouter as Router,
Switch,
Route,
Redirect
} from 'react-router-dom'
import { Navbar } from './app/Navbar'
import { PostsList } from './features/posts/PostsList'
function App() {
return (
<Router>
<Navbar />
<div className="App">
<Switch>
<Route
exact
path="/"
render={() => (
<React.Fragment>
<PostsList />
</React.Fragment>
)}
/>
<Redirect to="/" />
</Switch>
</div>
</Router>
)
}
export default App
これでRedux storeからデータをいくつか取り出して表示させることが出来ました。
Adding New Posts
記事を書いて保存し新しい投稿を追加してみましょう。
formとRedux storeを接続して新しい投稿を”Save Post”ボタンを押したときに追加します。
Adding the New Post Form
features/posts/AddPostForm.js
import React, { useState } from 'react'
export const AddPostForm = () => {
const [title, setTitle] = useState('')
const [content, setContent] = useState('')
const onTitleChanged = e => setTitle(e.target.value)
const onContentChanged = e => setContent(e.target.value)
return (
<section>
<h2>Add a New Post</h2>
<form>
<label htmlFor="postTitle">Post Title:</label>
<input
type="text"
id="postTitle"
name="postTitle"
value={title}
onChange={onTitleChanged}
/>
<label htmlFor="postContent">Content:</label>
<textarea
id="postContent"
name="postContent"
value={content}
onChange={onContentChanged}
/>
<button type="button">Save Post</button>
</form>
</section>
)
}
App.js
にそのコンポーネントをインポートして<PostsList />
の前に追加します。
App.js
<Route
exact
path="/"
render={() => (
<React.Fragment>
<AddPostForm />
<PostsList />
</React.Fragment>
)}
/>
Saving Post Entries
post sliceは投稿データの更新を制御しています。createSlice
の内側にreducers
と呼ばれるオブジェクトがあります。投稿を追加するケースを制御するためにreducer関数を追加しましょう。
reducers
の内側にpostAdded
という名前の関数を追加し2つの引数、現在のstate
とディスパッチするためのaction
オブジェクトを渡します。posts sliceは全体のRedux stateオブジェクトでなく投稿の配列のstate
に対してだけ責任を負います。
action
オブジェクトはaction.payload
フィールドに新規投稿エントリーを含みます。新規投稿をstate
配列に追加します。
postAdded
reducer関数を書いたときに、createSlice
は自動的に”action creator”関数を同じ名前で作成します。action creatorをエクスポートし、UIコンポーネントでそれを”Save Post”をクリックしたときactionをディスパッチして使います。
Dispatching the “Post Added” Action
AddPostForm
はテキスト入力と”Save Post”ボタンがありますが、ボタンはまだ何もしません。クリックハンドラーを追加して、psotAdded
action creatorをディスパッチしましょう、そしてユーザが書いたタイトルとコンテンツを含むオブジェクトを新規投稿として引数に渡します。
投稿するオブジェクトはid
フィールドを持っています。初期の投稿ではIDにフェイクナンバーを使います。ユニークな数値はランダムな数値を発生させるのがより良いので、Redux Toolkitのnanoid
関数を使います。
コンポーネントからactionsをディスパッチするために、ストアのdispatch
関数を経由する必要があります。React-ReduxからuseDispatch
フックを呼びます。
これでコンポーネントでdispatch
関数が利用できるようになりました。クリックハンドラーによってdispatch(postAdded())
を呼びましょう。タイトルとコンテンツの内容をReactコンポーネントのuseState
フックから取得し、新しいIDを生成して、それら一緒に新しい投稿オブジェクトとしてpostAdded()
に渡します。
features/posts/AddPostForm
import React, { useState } from 'react'
import { useDispatch } from 'react-redux'
import { nanoid } from '@reduxjs/toolkit'
import { postAdded } from './postsSlice'
export const AddPostForm = () => {
const [title, setTitle] = useState('')
const [content, setContent] = useState('')
const dispatch = useDispatch()
const onTitleChanged = e => setTitle(e.target.value)
const onContentChanged = e => setContent(e.target.value)
const onSavePostClicked = () => {
if (title && content) {
dispatch(
postAdded({
id: nanoid(),
title,
content
})
)
setTitle('')
setContent('')
}
}
return (
<section>
<h2>Add a New Post</h2>
<form>
{/* omit form inputs */}
<button type="button" onClick={onSavePostClicked}>
Save Post
</button>
</form>
</section>
)
}
ひとまず簡単なReact+Reduxアプリが出来ました。
完全なReduxデータフローサイクルを示します。
- 投稿リストは
useSelector
によりストアから初期の投稿リストを読み出し初期UIにレンダーします。 - 新しい投稿エントリーのデータを含んだ
postAdded
actionをディスパッチします。 - posts reducerは
postAdded
actionを見て、新しいエントリーの投稿の配列を更新します。 - Redux storeはデータにいくらかの変化があればUIに指示します。
- 投稿リスト更新された投稿の配列を読み取り、それ自身を新しい投稿を表示させるためにレンダリングします。
これが基本的なパターンです。slices of state を追加し、reducer関数を記述し、actionsをディスパッチし、Redux storeからのデータをもとにUIをレンダリングします。
気づいてほしいことはAddPostForm
コンポーネントはいくつかのReact useState
フックを使っているということです。それはタイトルと投稿内容の値をユーザがタイプしている間保持しています。思い出してほしいことはRedux storeはアプリケーションのための熟慮されたグローバルなデータだけを保持すべきだということです。