どうも、ぽっぽです。
今まで学んだ知識をもとに、より実践的なサンプルアプリケーションを作成してより深くReduxのことを学んでいきましょう。
小さなソーシャルメディアアプリを構築します。アプリケーションを構築することでReduxをどうやって使うか理解していきましょう。
Project Setup
公式が用意ししてくれるているGithub repoからローカルにクローンしましょう。npm installを実行し、npm startをタイプしてプロジェクトをスタートしましょう。
このプロジェクトの最終形態を見たい場合は、tutorial-steps branchを確認しましょう。
初期プロジェクトの中身はこのようになっています。
/srcindex.js:アプリケーションのエントリーポイントファイルです。React-Reduxの<Provider>コンポーネントとメインの<App>コンポーネントをレンダリングします。App.js:メインのアプリケーションコンポーネント。トップのナビバーとその他の中身をクライアントサイドルーチンを操作して描画します。index.css:完全なアプリケーションのスタイルを定義します。/apiclient.js:小さなAJAXリクエストのクライアント、GETとPOSTリクエストを送ることが出来ます。server.js:取得するデータのフェイクREST APIを提供。あとからフェイクエンドポイントからデータを取得します。
/appNavbar.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にレンダーします。 - 新しい投稿エントリーのデータを含んだ
postAddedactionをディスパッチします。 - posts reducerは
postAddedactionを見て、新しいエントリーの投稿の配列を更新します。 - Redux storeはデータにいくらかの変化があればUIに指示します。
- 投稿リスト更新された投稿の配列を読み取り、それ自身を新しい投稿を表示させるためにレンダリングします。
これが基本的なパターンです。slices of state を追加し、reducer関数を記述し、actionsをディスパッチし、Redux storeからのデータをもとにUIをレンダリングします。
気づいてほしいことはAddPostFormコンポーネントはいくつかのReact useStateフックを使っているということです。それはタイトルと投稿内容の値をユーザがタイプしている間保持しています。思い出してほしいことはRedux storeはアプリケーションのための熟慮されたグローバルなデータだけを保持すべきだということです。