
ReduxでシンプルなSNSを作る。公式Docsを読んでReduxのデータフローを学ぶ
最近Reduxを勉強してるんですけども、
公式ドキュメントの中に、簡単なSNSのフィード投稿フォームを作って、
Reduxのデータフローを学ぼうという項目があります。
これ、大変勉強になるんで、公式ドキュメントを翻訳しつつ、
僕の解釈とかも混ぜながら、この記事で解説してみたいと思います。
ちなみに、このドキュメントはパート3なので、
この記事を理解するには、基礎的なReduxの前提知識とかあるべきなので、
もしさっぱりという人は公式ドキュメントの最初から読んでみてほしいです。
Redux Essentials, Part 3: 基本的なReduxのデータフロー
学ぶこと
- createSliceを使ってReduxストアにReducerロジックの「スライス」を追加する方法
- useSelectorフックを使ってコンポーネントでReduxのデータを読み込む
- useDispatchフックでコンポーネント内のアクションをディスパッチする
前提条件
- 「アクション」、「レデューサ」、「ストア」、「ディスパッチ」など、Reduxの主要な用語や概念に精通していること
はじめに
パート1Reduxの概要とコンセプトでは、
アプリのグローバルな状態を一元的に管理できる場所を提供することで、
Reduxがどのように保守性の高いアプリの構築に役立つかを説明しました。
また、アクションオブジェクトのディスパッチ、
新しいステート値を返すReducer関数の使用、
Thunksを使用した非同期ロジックの記述など、
Reduxのコアコンセプトについても説明しました。
パート2 Reduxアプリの構造では、Redux ToolkitのconfigureStoreやcreateSlice、React-ReduxのProviderやuseSelectorなどのAPIが
どのように連携して、Reduxのロジックを書き、そのロジックをReactコンポーネントから操作することができるかを見ました。
さて、これらの部品が何であるかをある程度理解したところで、その知識を実践してみましょう。
これから小さなソーシャルメディアフィードアプリを作りますが、これには実世界のユースケースを示す多くの機能が含まれています。
これにより、自分のアプリケーションでReduxを使用する方法を理解することができます。
投稿フィード
ソーシャルメディアフィードアプリの主な機能は、投稿のリストです。
今後、この機能にいくつかのパーツを追加していく予定ですが、まず最初の目標は、画面上に投稿のリストだけを表示することです。
投稿一覧の作成
最初のステップは、投稿のデータを格納する新しいReduxの「スライス」を作成することです。
Reduxのストアにデータが入ったら、そのデータをページに表示するためのReactコンポーネントを作成します。
srcの中に新しいfeaturesフォルダを作成し、featuresの中にpostsフォルダを入れて、postsSlice.jsという新しいファイルを追加します。
Redux ToolkitのcreateSlice関数を使って、postsデータの処理方法を知っているreducer関数を作ります。
reducer関数には初期データが含まれている必要があり、アプリの起動時にReduxストアにこれらの値が読み込まれるようになっています。
ここでは、偽の投稿オブジェクトを含む配列を作成し、UIの追加を開始できるようにします。
createSliceをインポートして、最初のposts配列を定義し、それをcreateSliceに渡して、createSliceが生成したposts reducer関数をエクスポートします。
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ストアに追加する必要があります。
すでにReduxストアが作成されていますが、今は中にデータが入っていません。
app/store.jsを開き、postsReducer関数をインポートし、
postsReducerがpostsという名前のreducerフィールドとして渡されるようにconfigureStoreの呼び出しを更新します。
app/store.js
import { configureStore } from '@reduxjs/toolkit'
import postsReducer from '../features/posts/postsSlice'
export default configureStore({
reducer: {
posts: postsReducer
}
})
これはReduxに、トップレベルのstateオブジェクトの中にpostsというフィールドを持たせ、
アクションがディスパッチされたときにpostsReducer関数によってstate.postsのすべてのデータが更新されるように指示しています。
投稿リストの表示
ストアに投稿データができたので、投稿一覧を表示するReactコンポーネントを作成します。
フィードポスト機能に関連するコードはすべてpostsフォルダにあるはずなので、
そこにPostsList.jsという新しいファイルを作成します。
投稿のリストを表示するには、どこかからデータを取得する必要があります。
Reactコンポーネントは、React-ReduxライブラリのuseSelectorフックを使って、
Reduxストアからデータを読み込むことができます。
あなたが書いた「セレクタ関数」は、Reduxのステートオブジェクト全体をパラメータとして呼び出され、
このコンポーネントがストアから必要とする特定のデータを返さなければなりません。
最初のPostsListコンポーネントは、Reduxストアから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>
)
}
次に、App.jsのルーティングを更新して、
「ようこそ」というメッセージの代わりにPostsListコンポーネントを表示する必要があります。
PostsListコンポーネントをApp.jsにインポートして、welcomeテキストを<PostsList />に置き換えます。
また、このコンポーネントをReact Fragmentでラップします。
なぜなら、すぐにメインページに何かを追加する予定だからです。
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ストアに接続し、「Save Post」ボタンをクリックしたときに新しい記事が追加されるようにします。
postsフォルダにAddPostForm.jsを作成します。記事のタイトル用のテキスト入力と、記事の本文用のテキストエリアを追加します。
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 />コンポーネントのすぐ上に追加します。
<Route
exact
path="/"
render={() => (
<React.Fragment>
<AddPostForm />
<PostsList />
</React.Fragment>
)}
/
さて、新しい投稿エントリをReduxストアに追加するために、postスライスを更新しましょう。
postsスライスは、postsデータのすべての更新を処理する役割を担っています。
createSliceコールの中に、reducersというオブジェクトがあります。
今のところ、これは空です。ここにreducer関数を追加して、投稿が追加されたときの処理をする必要があります。
reducersの中に、postAddedという名前の関数を追加します。
この関数は2つの引数を受け取ります。
postsスライスは自分が担当しているデータしか知らないので、stateの引数はReduxのstateオブジェクト全体ではなく、postの配列そのものになります。
アクションオブジェクトは action.payload フィールドとして新しいポストエントリを持ち、その新しいポストオブジェクトを state 配列に入れます。
postAddedのリデューサ関数を書くと、createSliceは自動的に同じ名前の「アクションクリエイター」関数を生成します。
このアクションクリエーターをエクスポートして、UIコンポーネントで使用すれば、
ユーザーが「Save Post」をクリックしたときにアクションが実行されるようになります。
features/posts/postsSlice.js
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
postAdded(state, action) {
state.push(action.payload)
}
}
})
export const { postAdded } = postsSlice.actions
export default postsSlice.reducer
Post Added “アクションのディスパッチ”
AddPostFormには、テキスト入力と「Save Post」ボタンがありますが、ボタンはまだ何もしません。
クリックハンドラを追加して、postAddedアクションクリエータをディスパッチし、
ユーザが書いたタイトルとコンテンツを含む新しいポストオブジェクトを渡す必要があります。
投稿オブジェクトには、idフィールドが必要です。
現在、最初のテスト投稿では、IDに偽の数字を使っています。次の増分のID番号を把握するコードを書くこともできますが、
代わりにランダムなユニークIDを生成する方が良いでしょう。
Redux Toolkitにはnanoidという関数があるので、それを使うことができます。
コンポーネントからアクションをディスパッチするためには、ストアのディスパッチ関数にアクセスする必要があります。
これはReact-ReduxのuseDispatchフックを呼び出すことで得られます。
また、postAddedアクションクリエイターをこのファイルにインポートする必要があります。
コンポーネントでdispatch関数を利用できるようになったら、クリックハンドラでdispatch(postAdded())を呼び出すことができます。
ReactコンポーネントのuseStateフックからtitleとcontentの値を取得し、
新しいIDを生成して、postAdded()に渡す新しいpostオブジェクトにまとめることができます。
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のデータフローサイクルの全体像を示しています。
- posts listは、useSelectorでストアから最初の投稿セットを読み込み、初期のUIをレンダリングしました。
- 新しい投稿エントリのデータを含む postAdded アクションをディスパッチしました。
- posts reducer は postAdded アクションを見て、posts 配列を新しいエントリで更新しました。
- Redux ストアは、いくつかのデータが変更されたことを UI に伝えました。
- 投稿リストは、更新されたposts配列を読み、新しい投稿を表示するために再レンダリングします。
- この後に追加する新機能はすべて、ここまで見てきたのと同じ基本パターンに従います。ステートのスライスを追加し、リデューサ関数を書き、アクションを
- ディスパッチし、Reduxストアからのデータに基づいてUIをレンダリングします。