Redux Essentials、パート3:基本的なReduxデータフロー
createSlice
を使用して、Reduxストアにリデューサロジックのスライスを追加する方法useSelector
フックを使用して、コンポーネントでReduxデータを読み取る方法useDispatch
フックを使用して、コンポーネントでアクションをディスパッチする方法
- 「アクション」、「リデューサ」、「ストア」、「ディスパッチ」などの重要なRedux用語と概念に精通していること。(これらの用語の説明については、パート1:Reduxの概要と概念を参照してください。)
はじめに
パート1:Reduxの概要と概念では、Reduxがグローバルアプリ状態を配置する単一のセントラルリポジトリを提供することにより、保守可能なアプリの構築にどのように役立つのかを調べました。また、アクションオブジェクトのディスパッチ、新しい状態値を返すリデューサ関数の使用、thunkを使用した非同期ロジックの記述などのコアReduxの概念についても説明しました。パート2:Redux Toolkitアプリ構造では、Redux ToolkitのconfigureStore
およびcreateSlice
、React-ReduxのProvider
およびuseSelector
などのAPIが連携して、Reduxロジックを記述し、Reactコンポーネントからそのロジックと対話する方法を学びました。
これらの要素についてある程度の理解ができたので、その知識を実践する時です。いくつかの機能を含んだ小さなソーシャルメディアフィードアプリを構築します。これにより、独自のアプリケーションでReduxを使用する方法を理解するのに役立ちます。
サンプルアプリは、完全な本番対応プロジェクトとして意図されたものではありません。目標は、Redux APIと一般的な使用方法のパターンを学習し、限られた例を使用して正しい方向に導くことです。また、後でより良い方法を示すために、構築する初期のいくつかの部分は更新されます。すべての概念が使用されていることを確認するために、チュートリアル全体を読んでください。
プロジェクト設定
このチュートリアルでは、ReactとReduxが既に設定されており、デフォルトのスタイルが含まれており、アプリで実際のAPIリクエストを作成できるようにする偽のREST APIを含む、事前に構成されたスタータープロジェクトを作成しました。これは、実際のアプリケーションコードを作成するための基礎として使用します。
開始するには、このCodeSandboxを開いてフォークできます。
このGithubリポジトリから同じプロジェクトをクローンすることもできます。リポジトリをクローンしたら、npm install
でプロジェクトのツールをインストールし、npm start
で開始できます。
構築しようとしている最終バージョンを確認したい場合は、tutorial-steps
ブランチを確認するか、このCodeSandboxで最終バージョンを確認できます。
Tania Rasciaに感謝します。彼女のReactでのReduxの使用チュートリアルは、このページの例にインスピレーションを与えてくれました。また、スタイリングには彼女のPrimitive UI CSSスターターを使用しています。
新しいRedux + Reactプロジェクトの作成
このチュートリアルを終了したら、おそらく独自のプロジェクトに取り組みたいと思うでしょう。新しいRedux + Reactプロジェクトを作成する最速の方法として、ViteのReduxテンプレートを使用することをお勧めします。これは、パート1で見たのと同じ「カウンター」アプリの例を使用して、Redux ToolkitとReact-Reduxが既に構成されています。これにより、Reduxパッケージを追加してストアを設定する必要なく、実際のアプリケーションコードの記述にすぐに取り組むことができます。
プロジェクトにReduxを追加する方法の詳細を知りたい場合は、この説明を参照してください。
詳細な説明:ReactプロジェクトへのReduxの追加
ViteのReduxテンプレートには、Redux ToolkitとReact-Reduxが既に構成されています。そのテンプレートなしで最初から新しいプロジェクトを設定する場合は、次の手順に従ってください。
@reduxjs/toolkit
とreact-redux
パッケージを追加します。- RTKの
configureStore
APIを使用してReduxストアを作成し、少なくとも1つのリデューサ関数を渡します。 - Reduxストアをアプリケーションのエントリポイントファイル(
src/index.js
など)にインポートします。 - ルートReactコンポーネントをReact-Reduxの
<Provider>
コンポーネントでラップします。
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
初期プロジェクトの調査
初期プロジェクトの内容を簡単に見てみましょう。
/public
:HTMLホストページテンプレートと、アイコンなどのその他の静的ファイル。/src
index.js
:アプリケーションのエントリポイントファイル。React-Reduxの<Provider>
コンポーネントとメインの<App>
コンポーネントをレンダリングします。App.js
:メインアプリケーションコンポーネント。トップナビバーをレンダリングし、他のコンテンツのクライアントサイドルーティングを処理します。index.css
:アプリケーション全体のスタイル。/api
client.js
:GETとPOSTリクエストを行うことができる小さなAJAXリクエストクライアント。server.js
:データの偽のREST APIを提供します。私たちのアプリは後でこれらの偽のエンドポイントからデータを取得します。
/app
Navbar.js
:トップヘッダーとナビゲーションコンテンツをレンダリングします。store.js
:Reduxストアインスタンスを作成します。
アプリをロードすると、ヘッダーとウェルカムメッセージが表示されます。Redux DevTools拡張機能を開いて、初期のRedux状態が完全に空であることを確認することもできます。
それでは、始めましょう!
メイン投稿フィード
ソーシャルメディアフィードアプリの主な機能は、投稿のリストです。後でこの機能にいくつかの要素を追加しますが、まず、画面に投稿エントリのリストだけを表示することを目標とします。
投稿スライスの作成
最初のステップは、投稿のデータを含む新しいRedux「スライス」を作成することです。Reduxストアにデータがあるようになれば、そのデータをページに表示するReactコンポーネントを作成できます。
src
内に新しいfeatures
フォルダを作成し、features
の中にposts
フォルダを作成し、postsSlice.js
という名前の新しいファイルを追加します。
Redux ToolkitのcreateSlice
関数を使用して、投稿データの処理方法を理解するリデューサ関数を作成します。リデューサ関数は、アプリの起動時にReduxストアにそれらの値がロードされるように、いくつかの初期データを含める必要があります。
現時点では、UIの追加を開始できるように、いくつかの偽の投稿オブジェクトを含む配列を作成します。
createSlice
をインポートし、初期の投稿配列を定義し、それをcreateSlice
に渡し、createSlice
によって生成されたpostsリデューサ関数をエクスポートします。
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
関数をインポートし、configureStore
への呼び出しを更新して、postsReducer
がposts
という名前のリデューサフィールドとして渡されるようにします。
import { configureStore } from '@reduxjs/toolkit'
import postsReducer from '../features/posts/postsSlice'
export default configureStore({
reducer: {
posts: postsReducer
}
})
これは、最上位レベルの状態オブジェクトに内部にposts
という名前のフィールドを持たせ、アクションがディスパッチされると、state.posts
のすべてのデータがpostsReducer
関数によって更新されることをReduxに指示します。
Redux DevTools拡張機能を開いて現在の状態の内容を確認することで、これが機能することを確認できます。
投稿リストの表示
ストアに投稿データがあるので、投稿リストを表示するReactコンポーネントを作成できます。フィード投稿機能に関連するすべてのコードはposts
フォルダに配置する必要があります。そこで、PostsList.js
という名前の新しいファイルを作成します。
投稿リストをレンダリングするには、どこかからデータを取得する必要があります。Reactコンポーネントは、React-ReduxライブラリのuseSelector
フックを使用してReduxストアからデータを読み取ることができます。作成する「セレクター関数」は、Reduxのstate
オブジェクト全体を引数として呼び出され、このコンポーネントが必要とするストアからの特定のデータを返す必要があります。
最初のPostsList
コンポーネントは、Reduxストアからstate.posts
値を読み取り、投稿の配列をループして、それぞれを画面に表示します。
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>
)
}
次に、"ようこそ"メッセージの代わりにPostsList
コンポーネントを表示するように、App.js
のルーティングを更新する必要があります。PostsList
コンポーネントをApp.js
にインポートし、歓迎メッセージを<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ストアにデータを追加し、Reactコンポーネントで画面に表示しました。
新規投稿の追加
人々が書いた投稿を見るのは良いですが、自分の投稿も書けるようにしたいです。投稿を作成して保存できる「新規投稿の追加」フォームを作成しましょう。
まず空のフォームを作成し、ページに追加します。「投稿を保存」ボタンをクリックすると、新しい投稿がReduxストアに追加されるように、フォームをReduxストアに接続します。
新規投稿フォームの追加
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ストアに新しい投稿エントリを追加するように、postsスライスを更新しましょう。
postsスライスは、投稿データのすべての更新を処理する役割を担っています。createSlice
呼び出し内には、reducers
というオブジェクトがあります。現時点では空です。投稿の追加の場合を処理するために、そこにreducer関数を追加する必要があります。
reducers
内に、postAdded
という名前の関数を追加します。これは、現在のstate
値と、ディスパッチされたaction
オブジェクトの2つの引数を受け取ります。postsスライスは自分が担当するデータしか認識しないため、state
引数は投稿の配列自体になり、Redux stateオブジェクト全体にはなりません。
action
オブジェクトには、新しい投稿エントリがaction.payload
フィールドとして含まれ、この新しい投稿オブジェクトをstate
配列に入れます。
postAdded
reducer関数を記述すると、createSlice
は自動的に同じ名前の「アクションクリエイター」関数を生成します。このアクションクリエイターをエクスポートし、UIコンポーネントで使用して、ユーザーが「投稿を保存」をクリックしたときにアクションをディスパッチすることができます。
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
postAdded(state, action) {
state.push(action.payload)
}
}
})
export const { postAdded } = postsSlice.actions
export default postsSlice.reducer
覚えておいてください:**reducer関数は、常にコピーを作成することで、新しい状態値を不変的に作成する必要があります!**Array.push()
のような変更関数を呼び出したり、state.someField = someValue
のようにオブジェクトフィールドを変更したりすることは、Immerライブラリを使用して内部的にそれらの変更を安全な不変の更新に変換するため、createSlice()
内では安全ですが、**createSlice
の外でデータを変更しようとしないでください!**
「投稿が追加された」アクションのディスパッチ
AddPostForm
にはテキスト入力と「投稿を保存」ボタンがありますが、ボタンはまだ何も機能しません。postAdded
アクションクリエイターをディスパッチし、ユーザーが書いたタイトルとコンテンツを含む新しい投稿オブジェクトを渡すクリックハンドラーを追加する必要があります。
投稿オブジェクトにはid
フィールドも必要です。現時点では、初期のテスト投稿はIDにいくつかの偽の番号を使用しています。次のインクリメントID番号を計算するコードを書くこともできますが、代わりにランダムな一意のIDを生成する方が良いでしょう。Redux Toolkitには、それを使用できるnanoid
関数があります。
IDの生成とアクションのディスパッチについては、パート4:Reduxデータの使用で詳しく説明します。
コンポーネントからアクションをディスパッチするには、ストアのdispatch
関数にアクセスする必要があります。これは、React-ReduxのuseDispatch
フックを呼び出すことで取得します。このファイルにpostAdded
アクションクリエイターをインポートする必要もあります。
コンポーネントでdispatch
関数が使用可能になったら、クリックハンドラーでdispatch(postAdded())
を呼び出すことができます。ReactコンポーネントのuseState
フックからタイトルとコンテンツの値を取得し、新しいIDを生成し、それらを新しい投稿オブジェクトにまとめてpostAdded()
に渡すことができます。
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
アクションをディスパッチしました。 - posts reducerは
postAdded
アクションを確認し、新しいエントリを使用して投稿配列を更新しました。 - Reduxストアは、データが変更されたことをUIに伝えました。
- 投稿リストは更新された投稿配列を読み取り、新しい投稿を表示するために自身を再レンダリングしました。
これ以降に追加する新しい機能はすべて、ここで見た基本的なパターンに従います。状態のスライスの追加、reducer関数の記述、アクションのディスパッチ、Reduxストアからのデータに基づいたUIのレンダリングです。
Redux DevTools Extensionを使用して、ディスパッチしたアクションを確認し、そのアクションに応答してReduxの状態がどのように更新されたかを確認できます。「アクション」リストで"posts/postAdded"
エントリをクリックすると、「アクション」タブは次のようになります。
「Diff」タブには、state.posts
に1つの新しいアイテムが追加され、インデックス2にあることも表示されます。
AddPostForm
コンポーネントには、ユーザーが入力しているタイトルとコンテンツの値を追跡するために、いくつかのReact useState
フックが含まれていることに注意してください。覚えておいてください。**Reduxストアには、アプリケーションの「グローバル」と見なされるデータのみを含める必要があります!**この場合、AddPostForm
のみが入力フィールドの最新の値を知る必要があるため、Reduxストアに一時データを保持しようとするのではなく、そのデータをReactコンポーネントの状態に保持したいと考えています。ユーザーがフォームを終了したら、Reduxアクションをディスパッチして、ユーザー入力に基づいてストアの最終値を更新します。
学習した内容
このセクションで学習した内容を要約しましょう。
- Reduxの状態は「reducer関数」によって更新されます。:
- Reducerは常に、既存の状態値をコピーし、新しいデータでコピーを変更することで、新しい状態を*不変的に*計算します。
- Redux Toolkitの
createSlice
関数は、ユーザーのために「スライスreducer」関数を生成し、「変更」コードを安全な不変の更新に変換できるようにします。 - これらのスライスreducer関数は、
configureStore
のreducer
フィールドに追加され、これによりReduxストア内のデータと状態フィールド名が定義されます。
- Reactコンポーネントは、
useSelector
フックを使用してストアからデータを読み取ります。- セレクター関数は、
state
オブジェクト全体を受け取り、値を返す必要があります。 - Reduxストアが更新されるたびにセレクターは再実行され、返されたデータが変更された場合、コンポーネントは再レンダリングされます。
- セレクター関数は、
- Reactコンポーネントは、
useDispatch
フックを使用してアクションをディスパッチしてストアを更新します。createSlice
は、スライスに追加する各reducerに対してアクションクリエイター関数を生成します。- コンポーネントで
dispatch(someActionCreator())
を呼び出して、アクションをディスパッチします。 - Reducerは実行され、このアクションが関連しているかどうかを確認し、適切な場合は新しい状態を返します。
- フォーム入力値などの一時データは、Reactコンポーネントの状態として保持する必要があります。ユーザーがフォームを終了したら、Reduxアクションをディスパッチしてストアを更新します。
今のところ、アプリは次のようになります。
次のステップ
基本的なReduxデータフローを理解したので、パート4:Reduxデータの使用に進み、アプリに追加の機能を追加し、すでにストアにあるデータの使用方法の例を確認しましょう。