Redux 基礎 Part 3: ステート、アクション、リデューサー
このページは PageTurner AI で翻訳されました(ベータ版)。プロジェクト公式の承認はありません。 エラーを見つけましたか? 問題を報告 →
- アプリのデータを保持するステート値の定義方法
- アプリ内で発生する事象を記述するアクションオブジェクトの定義方法
- 既存ステートとアクションに基づいて更新ステートを計算するリデューサー関数の記述方法
- "アクション"、"リデューサー"、"ストア"、"ディスパッチ"といった主要なRedux用語と概念の理解(これらの用語の説明はPart 2: Reduxの概念とデータフロー参照)
はじめに
Part 2: Reduxの概念とデータフローでは、グローバルなアプリケーションステートを一元管理する場所を提供することでReduxが保守性の高いアプリ構築をどのように支援するかを見てきました。また、アクションオブジェクトのディスパッチや新しいステート値を返すリデューサー関数の使用といったReduxのコア概念についても説明しました。
これらの構成要素について理解したところで、知識を実践に移す時が来ました。これらの部品が実際にどのように連携するかを確認するために、小さなサンプルアプリを構築していきます。
このページは PageTurner AI で翻訳されました(ベータ版)。プロジェクト公式の承認はありません。 エラーを見つけましたか? 問題を報告 →
このチュートリアルでは、Reduxの基本原則と概念を説明するために、あえて旧来のスタイルのReduxロジックパターンを使用しています。これらは、現代的なReduxアプリ開発の正しいアプローチとして推奨しているRedux Toolkitを使った「モダンRedux」パターンに比べてコード量が多くなります。このチュートリアルは_プロダクション環境で使用することを想定したものではありません_。
「モダンRedux」をRedux Toolkitで実践する方法については、以下のページを参照してください:
- 完全版「Redux Essentials」チュートリアル: 現実世界のアプリケーションでRedux Toolkitを使った「Reduxの正しい使い方」を解説しています。すべてのRedux学習者は「Essentials」チュートリアルの読了を推奨します!
- Redux Fundamentals パート8: Redux ToolkitによるモダンRedux: 前半セクションのローレベルな例をモダンなRedux Toolkitの実装に変換する方法を解説
プロジェクト設定
このチュートリアルでは、Reactのセットアップが済み、デフォルトのスタイリングが含まれ、実際のAPIリクエストをアプリ内で記述できる偽のREST APIを備えた事前設定済みのスタータープロジェクトを用意しました。実際のアプリケーションコードを記述する基盤としてこれを使用します。
開始するには、こちらのCodeSandboxを開いてフォークしてください:
またこのGitHubリポジトリから同じプロジェクトをクローンすることもできます。リポジトリをクローンした後、npm installでプロジェクトのツールをインストールし、npm startで起動できます。
構築予定の最終版を確認したい場合は、tutorial-stepsブランチをチェックするか、このCodeSandboxの最終版をご覧ください。
新しい Redux + React プロジェクトの作成
このチュートリアルを完了したら、独自のプロジェクトに取り組みたくなるでしょう。新しいRedux + Reactプロジェクトを作成する最速の方法としてCreate-React-App向けReduxテンプレートの使用をお勧めします。これにはRedux ToolkitとReact-Reduxが既に設定されており、Part 1で見た「カウンター」アプリの現代化バージョンを使用しています。これにより、Reduxパッケージを追加したりストアをセットアップしたりせずに、実際のアプリケーションコードの記述にすぐに取り掛かれます。
プロジェクトにReduxを追加する具体的な方法については、以下の説明を参照してください:
Detailed Explanation: Adding Redux to a React Project
The Redux template for CRA comes with Redux Toolkit and React-Redux already configured. If you're setting up a new project from scratch without that template, follow these steps:
- Add the
@reduxjs/toolkitandreact-reduxpackages - Create a Redux store using RTK's
configureStoreAPI, and pass in at least one reducer function - Import the Redux store into your application's entry point file (such as
src/index.js) - Wrap your root React component with the
<Provider>component from React-Redux, like:
root.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
初期プロジェクトの確認
この初期プロジェクトは、いくつかの変更を加えた標準のViteプロジェクトテンプレートを基にしています。
初期プロジェクトに含まれているものを簡単に見てみましょう:
/srcindex.js: アプリケーションのエントリーポイントファイル。メインの<App>コンポーネントをレンダリングするApp.js: メインアプリケーションコンポーネントindex.css: アプリケーション全体のスタイル/apiclient.js: HTTP GETおよびPOSTリクエストを作成できるfetchラッパークライアントserver.js: データ用の偽のREST APIを提供。後ほどアプリがこれらの偽エンドポイントからデータを取得する
/exampleAddons: チュートリアル後半で動作を説明するために使用する追加のReduxアドオンを含む
アプリを今読み込むと、ウェルカムメッセージは表示されますが、それ以外の部分は空です。
では、始めましょう!
Todoサンプルアプリの開始
例として、小さな「todo(タスク管理)」アプリケーションを作成します。おそらくtodoアプリの例を以前に見たことがあるでしょう。これらは良い教材となります。なぜなら、アイテムリストの追跡、ユーザー入力の処理、データ変更時のUI更新といった、通常のアプリケーションで発生する操作を実演できるからです。
要件の定義
まず、このアプリケーションの初期ビジネス要件を整理しましょう:
-
UIは次の3つの主要セクションで構成される:
- 新しいtodoアイテムのテキストを入力する入力ボックス
- 既存の全todoアイテムを表示するリスト
- 未完了todoの数を表示し、フィルタリングオプションを提供するフッターセクション
-
Todoリストアイテムには以下が必要:
- 「完了」ステータスを切り替えるチェックボックス
- 事前定義された色リストからカラーコード化されたカテゴリタグを追加する機能
- todoアイテムを削除する機能
-
カウンターはアクティブなtodoの数を複数形で表示:「0 items」「1 item」「3 items」など
-
全todoを完了済みとしてマークするボタン
-
完了済みtodoを一括削除するボタン
-
リスト表示のtodoをフィルタリングする2つの方法:
- 「すべて」「アクティブ」「完了済み」でフィルタリング
- 1つ以上の色を選択し、一致するタグを持つtodoを表示
後でさらに要件を追加しますが、開始するにはこれで十分です。
最終的なアプリケーションの外観は次のようになります:

状態の値の設計
ReactとReduxの中核原則の1つはUIが状態に基づくべきという点です。アプリケーション設計ではまず、アプリケーションの動作を記述するために必要な全状態を考えるアプローチが有効です。また、状態内の値を最小限に抑えてUIを記述することも重要です。これにより追跡・更新すべきデータ量が減少します。
概念的には、このアプリケーションに2つの主要な側面があります:
-
現在のtodoアイテムの実リスト
-
現在のフィルタリングオプション
ユーザーが「Add Todo」入力ボックスに入力中のデータも追跡しますが、これは重要度が低いため後で扱います。
各todoアイテムに必要な情報は:
-
ユーザーが入力したテキスト
-
完了状態を示すブール値フラグ
-
一意のID値
-
選択されている場合の色カテゴリ
フィルタリング動作は次の列挙値で記述できます:
-
完了ステータス:「All」「Active」「Completed」
-
色:「Red」「Yellow」「Green」「Blue」「Orange」「Purple」
これらの値を分類すると、todosは「アプリ状態」(アプリケーションが操作するコアデータ)であり、フィルタリング値は「UI状態」(アプリの現在の動作を記述する状態)と言えます。これらのカテゴリを区別することで、状態の各部分がどのように使用されるかを理解しやすくなります。
状態構造の設計
Reduxではアプリケーション状態は常にプレーンなJavaScriptオブジェクトと配列で保持されます。つまりRedux状態に含めてはいけないものは:
- クラスインスタンス
Map/Set/Promise/Dateなどの組み込みJS型- 関数
- プレーンなJSデータではないもの
ルートのRedux状態値はほぼ常にプレーンなJSオブジェクトであり、他のデータはその中にネストされます。
この情報に基づいて、Reduxのstate内に持つ必要がある値の種類を説明できるはずです:
-
まず、todo項目のオブジェクト配列が必要です。各項目には以下のフィールドが必要です:
id: 一意の数値text: ユーザーが入力したテキストcompleted: 真偽値フラグcolor: オプションのカラーカテゴリ
-
次に、フィルタリングオプションを設定する必要があります:
- 現在の「完了」フィルタ値
- 現在選択されているカラーカテゴリの配列
したがって、アプリのstateの例は以下のようになります:
const todoAppState = {
todos: [
{ id: 0, text: 'Learn React', completed: true },
{ id: 1, text: 'Learn Redux', completed: false, color: 'purple' },
{ id: 2, text: 'Build something fun!', completed: false, color: 'blue' }
],
filters: {
status: 'Active',
colors: ['red', 'blue']
}
}
Reduxの外部に他のstateがあっても問題ないことに注意することが重要です!この例は現時点で十分小さいため、実際にすべてのstateをReduxストアに保持していますが、後ほど見るように、一部のデータ(「このドロップダウンは開いているか?」や「フォーム入力の現在の値」など)はReduxに保持する必要がまったくありません。
アクションの設計
アクションはtypeフィールドを持つプレーンなJavaScriptオブジェクトです。前述のように、アクションはアプリケーションで発生した何かを記述するイベントと考えることができます。
アプリの要件に基づいてstateの構造を設計したのと同じように、発生していることを記述するアクションのリストを作成できます:
-
ユーザーが入力したテキストに基づいて新しいtodoを追加する
-
todoの完了状態を切り替える
-
todoのカラーカテゴリを選択する
-
todoを削除する
-
すべてのtodoを完了済みとしてマークする
-
完了済みのtodoをすべてクリアする
-
異なる「完了」フィルタ値を選択する
-
新しいカラーフィルタを追加する
-
カラーフィルタを削除する
通常、発生したことを記述するために必要な追加データはaction.payloadフィールドに置きます。これは数値、文字列、または複数のフィールドを持つオブジェクトが可能です。
Reduxストアはaction.typeフィールドの実際のテキストを気にしません。ただし、更新が必要かどうかを確認するために、自身のコードがaction.typeを参照します。また、デバッグ時にRedux DevTools Extensionでアクションタイプの文字列を頻繁に確認し、アプリで何が起こっているかを把握します。したがって、読みやすく、何が起こっているかを明確に記述するアクションタイプを選択するようにしてください。後で見たときに理解しやすくなります!
発生する可能性のあるもののリストに基づいて、アプリケーションが使用するアクションのリストを作成できます:
-
{type: 'todos/todoAdded', payload: todoText} -
{type: 'todos/todoToggled', payload: todoId} -
{type: 'todos/colorSelected', payload: {todoId, color}} -
{type: 'todos/todoDeleted', payload: todoId} -
{type: 'todos/allCompleted'} -
{type: 'todos/completedCleared'} -
{type: 'filters/statusFilterChanged', payload: filterValue} -
{type: 'filters/colorFilterChanged', payload: {color, changeType}}
この場合、アクションには主に1つの追加データがあるため、それを直接action.payloadフィールドに置くことができます。カラーフィルタの動作を「追加」と「削除」の2つのアクションに分割することもできましたが、ここではアクションペイロードとしてオブジェクトを持つことができることを示すために、内部に追加フィールドを持つ1つのアクションとして実装します。
stateデータと同様に、アクションは発生したことを記述するために必要な最小限の情報を含めるべきです。
リデューサーの作成
stateの構造とactionの形式がわかったので、最初のリデューサーを作成しましょう。
リデューサーは現在のstateとactionを引数として受け取り、新しいstateの結果を返す関数です。つまり (state, action) => newState という形式になります。
ルートリデューサーの作成
Reduxアプリケーションには実際には1つのリデューサー関数しかありません。後ほどcreateStoreに渡す「ルートリデューサー」関数です。この1つのルートリデューサー関数が、dispatchされる_すべての_actionを処理し、毎回の_全体の_新しいstateの結果を計算する役割を担います。
まずsrcフォルダ内にindex.jsとApp.jsと同じ階層にreducer.jsファイルを作成しましょう。
すべてのリデューサーには初期stateが必要なので、開始用にいくつかのダミーのtodo項目を追加します。その後、リデューサー関数内のロジックの概略を作成します:
const initialState = {
todos: [
{ id: 0, text: 'Learn React', completed: true },
{ id: 1, text: 'Learn Redux', completed: false, color: 'purple' },
{ id: 2, text: 'Build something fun!', completed: false, color: 'blue' }
],
filters: {
status: 'All',
colors: []
}
}
// Use the initialState as a default value
export default function appReducer(state = initialState, action) {
// The reducer normally looks at the action type field to decide what happens
switch (action.type) {
// Do something here based on the different types of actions
default:
// If this reducer doesn't recognize the action type, or doesn't
// care about this specific action, return the existing state unchanged
return state
}
}
アプリケーションの初期化時、リデューサーはstate値としてundefinedが渡されて呼び出される場合があります。その場合、リデューサーコードの残りが処理できるように初期state値を提供する必要があります。通常、リデューサーはデフォルト引数構文を使用して初期stateを提供します:(state = initialState, action)。
次に、'todos/todoAdded'アクションを処理するロジックを追加しましょう。
まず現在のactionのtypeが特定の文字列と一致するか確認する必要があります。 その後、変更されていないフィールドも含む_すべての_stateを含む新しいオブジェクトを返す必要があります。
function nextTodoId(todos) {
const maxId = todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1)
return maxId + 1
}
// Use the initialState as a default value
export default function appReducer(state = initialState, action) {
// The reducer normally looks at the action type field to decide what happens
switch (action.type) {
// Do something here based on the different types of actions
case 'todos/todoAdded': {
// We need to return a new state object
return {
// that has all the existing state data
...state,
// but has a new array for the `todos` field
todos: [
// with all of the old todos
...state.todos,
// and the new todo object
{
// Use an auto-incrementing numeric ID for this example
id: nextTodoId(state.todos),
text: action.payload,
completed: false
}
]
}
}
default:
// If this reducer doesn't recognize the action type, or doesn't
// care about this specific action, return the existing state unchanged
return state
}
}
これは...stateに1つのtodo項目を追加するには非常に手間がかかりますね。なぜこれほど多くの追加作業が必要なのでしょうか?
リデューサーのルール
先述したように、リデューサーは_常に_いくつかの特別なルールに従う必要があります:
-
stateとaction引数に基づいてのみ新しい状態値を計算する -
既存の
stateを変更してはならない。代わりに、既存のstateをコピーしコピーした値を変更するという不変的な更新を行う -
非同期ロジックや他の「副作用」を実行してはいけない
「副作用」とは、関数から値を返すこと以外に見られる状態や動作の変更のことです。一般的な副作用の種類には以下があります:
- コンソールへの値の出力
- ファイルの保存
- 非同期タイマーの設定
- HTTPリクエストの実行
- 関数の外部に存在する状態の変更、または関数への引数の変更
- 乱数や一意のランダムIDの生成(例:
Math.random()やDate.now())
これらのルールに従う関数は、リデューサー関数として特別に書かれていなくても、「純粋関数」 として知られています。
しかし、なぜこれらのルールが重要なのでしょうか?いくつかの理由があります:
-
Redux の目標の1つはコードの予測可能性を高めることです。関数の出力が入力引数のみから計算される場合、そのコードの動作を理解しテストするのが容易になります。
-
一方で、関数が外部変数に依存していたりランダムな動作をする場合、実行時に何が起こるか予測できません。
-
関数が引数を含む他の値を変更すると、アプリケーションの動作が予期せず変化することがあります。これは「状態を更新したのにUIが更新されない!」といったバグの一般的な原因となり得ます。
-
Redux DevToolsの一部の機能は、あなたのリデューサーがこれらのルールを正しく守っていることに依存しています
「不変の更新」に関するルールは特に重要であり、さらに詳しく説明する価値があります。
リデューサーと不変更新
先ほど、「ミューテーション」(既存のオブジェクト/配列値の変更)と「イミュータビリティ」(値を変更不可能なものとして扱うこと)について説明しました。
Reduxでは、リデューサーが元の/現在のstate値を変更することは_絶対に_許可されていません!
// ❌ Illegal - by default, this will mutate the state!
state.value = 123
Reduxで状態を変更してはいけない理由はいくつかあります:
-
UIが最新値を正しく表示しないなどのバグを引き起こす
-
状態がなぜ・どのように更新されたかを理解しにくくなる
-
テストの作成が困難になる
-
「タイムトラベルデバッグ」機能が正しく動作しなくなる
-
Reduxの設計思想と使用パターンに反する
では、元の値を変更できない場合、更新された状態をどのように返せばよいのでしょうか?
リデューサーは元の値のコピーを作成し、そのコピーを変更できます。
// ✅ This is safe, because we made a copy
return {
...state,
value: 123
}
JavaScriptの配列/オブジェクトスプレッド演算子や、元の値のコピーを返す他の関数を使用することで、手動で不変更新を書く方法は既に見ました。
これはデータがネストされている場合に難しくなります。不変更新の重要なルールは、更新が必要なネストの_すべての_レベルをコピーしなければならないことです。
しかし、「手動でこのように不変更新を書くのは覚えにくく、正しく行うのが難しい」と思っているなら...ええ、その通りです! :)
手動で不変更新ロジックを書くことは確かに難しく、リデューサー内での誤ったstate変更はReduxユーザーが最もよく犯すミスです。
実際のアプリケーションでは、このような複雑なネストされた不変更新を手作業で書く必要はありません。パート8: Redux Toolkitを使ったモダンなReduxでは、リデューサー内で不変更新ロジックを簡略化するためにRedux Toolkitを使用する方法を学びます。
追加のアクションの処理
このことを念頭に置いて、さらにいくつかのケースに対するリデューサーロジックを追加しましょう。まず、IDに基づいてtodoのcompletedフィールドをトグルする処理です:
export default function appReducer(state = initialState, action) {
switch (action.type) {
case 'todos/todoAdded': {
return {
...state,
todos: [
...state.todos,
{
id: nextTodoId(state.todos),
text: action.payload,
completed: false
}
]
}
}
case 'todos/todoToggled': {
return {
// Again copy the entire state object
...state,
// This time, we need to make a copy of the old todos array
todos: state.todos.map(todo => {
// If this isn't the todo item we're looking for, leave it alone
if (todo.id !== action.payload) {
return todo
}
// We've found the todo that has to change. Return a copy:
return {
...todo,
// Flip the completed flag
completed: !todo.completed
}
})
}
}
default:
return state
}
}
また、これまでtodosのステートに焦点を当ててきたので、「可視性フィルター変更」アクションを処理するケースも追加しましょう:
export default function appReducer(state = initialState, action) {
switch (action.type) {
case 'todos/todoAdded': {
return {
...state,
todos: [
...state.todos,
{
id: nextTodoId(state.todos),
text: action.payload,
completed: false
}
]
}
}
case 'todos/todoToggled': {
return {
...state,
todos: state.todos.map(todo => {
if (todo.id !== action.payload) {
return todo
}
return {
...todo,
completed: !todo.completed
}
})
}
}
case 'filters/statusFilterChanged': {
return {
// Copy the whole state
...state,
// Overwrite the filters value
filters: {
// copy the other filter fields
...state.filters,
// And replace the status field with the new value
status: action.payload
}
}
}
default:
return state
}
}
まだ3つのアクションしか処理していませんが、すでにコードが長くなりつつあります。この単一のリデューサー関数ですべてのアクションを処理しようとすると、全体を読み取るのが難しくなるでしょう。
そのため、リデューサーは通常、複数の小さなリデューサー関数に分割されます。これによりリデューサーロジックの理解と保守が容易になります。
リデューサーの分割
この一環として、Reduxリデューサーは通常、更新対象となるReduxステートのセクションに基づいて分割されます。私たちのtodoアプリステートには現在、state.todosとstate.filtersという2つのトップレベルセクションがあります。したがって、大きなルートリデューサー関数をtodosReducerとfiltersReducerという2つの小さなリデューサーに分割できます。
では、これらの分割されたリデューサー関数はどこに配置すべきでしょうか?
Reduxアプリのフォルダとファイルは「フィーチャー」(機能単位)に基づいて整理することをお勧めします。これはアプリケーションの特定の概念や領域に関連するコードです。特定のフィーチャーに関するReduxコードは通常、「スライス」ファイルと呼ばれる単一ファイルに記述され、アプリステートの該当部分に関するすべてのリデューサーロジックとアクション関連コードが含まれます。
このため、Reduxアプリステートの特定セクションを担当するリデューサーは「スライスリデューサー」と呼ばれます。通常、一部のアクションオブジェクトは特定のスライスリデューサーと密接に関連するため、アクションタイプ文字列は'todos'のようなフィーチャー名で始まり、'todoAdded'のような発生イベントを説明し、'todos/todoAdded'のように結合されます。
プロジェクト内にfeaturesフォルダを作成し、その中にtodosフォルダを作ります。新しいファイルtodosSlice.jsを作成し、todo関連の初期ステートをこのファイルに切り取って貼り付けましょう:
const initialState = [
{ id: 0, text: 'Learn React', completed: true },
{ id: 1, text: 'Learn Redux', completed: false, color: 'purple' },
{ id: 2, text: 'Build something fun!', completed: false, color: 'blue' }
]
function nextTodoId(todos) {
const maxId = todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1)
return maxId + 1
}
export default function todosReducer(state = initialState, action) {
switch (action.type) {
default:
return state
}
}
次にtodo更新ロジックをコピーします。ただし、重要な違いがあります。このファイルはtodo関連ステートのみを更新すればよく、もはやネストされていません! これがリデューサー分割のもう一つの利点です。todosステート自体が配列であるため、外側のルートステートオブジェクトをコピーする必要がなく、リデューサーが読みやすくなります。
これはリデューサーの合成と呼ばれ、Reduxアプリ構築の基本パターンです。
これらのアクションを処理した後の更新されたリデューサーは次のようになります:
export default function todosReducer(state = initialState, action) {
switch (action.type) {
case 'todos/todoAdded': {
// Can return just the new todos array - no extra object around it
return [
...state,
{
id: nextTodoId(state),
text: action.payload,
completed: false
}
]
}
case 'todos/todoToggled': {
return state.map(todo => {
if (todo.id !== action.payload) {
return todo
}
return {
...todo,
completed: !todo.completed
}
})
}
default:
return state
}
}
少し短くなり、読みやすくなりました。
可視性フィルターロジックに対しても同様の処理を行いましょう。src/features/filters/filtersSlice.jsを作成し、フィルター関連コードをすべて移動します:
const initialState = {
status: 'All',
colors: []
}
export default function filtersReducer(state = initialState, action) {
switch (action.type) {
case 'filters/statusFilterChanged': {
return {
// Again, one less level of nesting to copy
...state,
status: action.payload
}
}
default:
return state
}
}
フィルターステートを含むオブジェクトのコピーは依然として必要ですが、ネストが浅いため処理内容が読み取りやすくなっています。
このページを簡潔にするため、他のアクションに対するリデューサー更新ロジックの記述は割愛します。
前述の要件に基づいて、これらの更新を自身で記述してみてください。
行き詰まった場合は、このページ末尾のCodeSandboxでこれらのリデューサーの完全な実装を参照してください。
リデューサーの結合
これで2つの独立したスライスファイルができました。それぞれに独自のスライスリデューサー関数があります。しかし、Reduxストアの作成時には単一のルートリデューサー関数が必要であると前述しました。では、すべてのコードを1つの大きな関数にまとめることなく、どのようにルートリデューサーを実現すればよいでしょうか?
リデューサーは通常のJavaScript関数なので、スライスリデューサーをreducer.jsにインポートし、他の2つの関数を呼び出すだけの新しいルートリデューサーを作成できます。
import todosReducer from './features/todos/todosSlice'
import filtersReducer from './features/filters/filtersSlice'
export default function rootReducer(state = {}, action) {
// always return a new object for the root state
return {
// the value of `state.todos` is whatever the todos reducer returns
todos: todosReducer(state.todos, action),
// For both reducers, we only pass in their slice of the state
filters: filtersReducer(state.filters, action)
}
}
各リデューサーはグローバルステートの一部を管理していることに注意してください。stateパラメータはリデューサーごとに異なり、それぞれが管理するステートの部分に対応します。
これにより、機能やステートのスライスに基づいてロジックを分割し、保守性を保つことができます。
combineReducers
新しいルートリデューサーは各スライスに対して同じ処理を行っていることがわかります:スライスリデューサーを呼び出し、そのリデューサーが所有するステートのスライスを渡し、結果をルートステートオブジェクトに割り当てます。さらにスライスを追加する場合も、このパターンが繰り返されます。
ReduxコアライブラリにはcombineReducersというユーティリティが含まれており、この定型処理を代行してくれます。手書きのrootReducerをcombineReducersで生成された簡潔なものに置き換えることができます。
combineReducersが必要になったので、Reduxコアライブラリを実際にインストールするタイミングです:
npm install redux
インストールが完了したら、combineReducersをインポートして使用します:
import { combineReducers } from 'redux'
import todosReducer from './features/todos/todosSlice'
import filtersReducer from './features/filters/filtersSlice'
const rootReducer = combineReducers({
// Define a top-level state field named `todos`, handled by `todosReducer`
todos: todosReducer,
filters: filtersReducer
})
export default rootReducer
combineReducersはオブジェクトを受け取り、そのキー名がルートステートオブジェクトのキーになります。値はReduxステートの該当スライスを更新する方法を知っているスライスリデューサー関数です。
combineReducersに渡すキー名が、ステートオブジェクトのトップレベルキーを決定することを覚えておいてください!
学んだこと
ステート、アクション、リデューサーはReduxの基本構成要素です。すべてのReduxアプリはステート値を持ち、発生したことを記述するアクションを作成し、前のステートとアクションに基づいて新しいステート値を計算するリデューサー関数を使用します。
現在のアプリの内容は以下の通りです:
- ReduxアプリはプレーンなJSオブジェクト、配列、プリミティブをステート値として使用する
- ルートステート値はプレーンなJSオブジェクトであるべき
- ステートにはアプリ動作に必要な最小限のデータのみ含める
- クラス、Promise、関数など非プレーン値はReduxステートに含めない
- リデューサーは
Math.random()やDate.now()のようなランダムな値を生成してはならない - Redux以外のステート値(コンポーネントのローカルステートなど)をReduxと併用するのは問題ない
- アクションは発生した内容を記述する
typeフィールドを持つプレーンオブジェクトtypeフィールドは読み取り可能な文字列で、通常'feature/eventName'形式- 他の値は
action.payloadフィールドに格納 - アクションには発生内容を記述する最小限のデータのみ含める
- リデューサーは
(state, action) => newState形式の関数- リデューサーは常に特別なルールに従う必要がある:
stateとaction引数のみに基づいて新しいステートを計算- 既存の
stateを変更せず、常にコピーを返す - HTTPリクエストや非同期ロジックなどの「副作用」を起こさない
- リデューサーは常に特別なルールに従う必要がある:
- リデューサーは可読性向上のために分割する
- 通常、トップレベルのステートキーやステートの「スライス」単位で分割
- 「スライス」ファイルに記述し、「機能」フォルダに整理
- Reduxの
combineReducers関数でリデューサーを結合可能 combineReducersに渡すキー名がトップレベルステートオブジェクトのキーを定義
次のステップ
ステートを更新するリデューサーロジックができましたが、これらだけでは何も実行されません。リデューサーをReduxストア内に配置し、何かが発生したときにアクションと共にリデューサーコードを呼び出せるようにする必要があります。
パート4: ストア では、Reduxストアの作成方法とリデューサーロジックの実行方法について説明します。