メインコンテンツにスキップ

Reduxの基礎、パート3:ステート、アクション、リデューサー

Reduxの基礎、パート3:ステート、アクション、リデューサー

学習内容
  • アプリのデータを含むステート値を定義する方法
  • アプリ内で何が起こるかを記述するアクションオブジェクトを定義する方法
  • 既存のステートとアクションに基づいて更新されたステートを計算するリデューサー関数を記述する方法
前提条件
  • 「アクション」、「リデューサー」、「ストア」、「ディスパッチ」などの主要なRedux用語と概念に精通していること。(これらの用語の説明については、パート2:Reduxの概念とデータフローを参照してください。)

はじめに

パート2:Reduxの概念とデータフローでは、Reduxがグローバルアプリステートを配置する単一の中心的な場所を提供することにより、保守しやすいアプリを構築するのにどのように役立つかを見てきました。また、アクションオブジェクトのディスパッチや、新しいステート値を返すリデューサー関数などのコアReduxの概念についても説明しました。

これらの要素が何かを理解したので、その知識を実践に移す時が来ました。これらの要素が実際にどのように連携するかを確認するために、小さなサンプルアプリを作成します。

注意

このチュートリアルでは、Reduxの背後にある原則と概念を説明するために、今日Reduxでアプリを構築するための正しいアプローチとして教えるRedux Toolkitを使用した「モダンRedux」パターンよりも多くのコードを必要とする、古いスタイルのReduxロジックパターンを意図的に示していることに注意してください。これは、本番環境に対応したプロジェクトを意味するものではありません

Redux Toolkitを使用した「モダンRedux」の使用方法については、これらのページを参照してください

プロジェクトのセットアップ

このチュートリアルでは、Reactがすでに設定され、デフォルトのスタイルが含まれ、アプリで実際のAPIリクエストを記述できるようにする偽のREST APIを備えた、事前構成済みのスタータープロジェクトを作成しました。これを、実際のアプリケーションコードを記述するための基礎として使用します。

開始するには、このCodeSandboxを開いてフォークできます

また、このGithubリポジトリから同じプロジェクトをクローンすることもできます。リポジトリをクローンした後、npm installでプロジェクトのツールをインストールし、npm startで開始できます。

構築するものの最終バージョンを確認する場合は、tutorial-stepsブランチをチェックするか、このCodeSandboxで最終バージョンを確認できます

新しいRedux + Reactプロジェクトの作成

このチュートリアルを終えると、おそらく自分のプロジェクトに取り組んでみたくなるでしょう。新しいRedux + Reactプロジェクトを作成する最も速い方法として、Create-React-App用のReduxテンプレートを使用することをお勧めします。これには、パート1で見た「カウンター」アプリの例を近代化したバージョンを使用して、Redux ToolkitとReact-Reduxがすでに構成されています。これにより、Reduxパッケージを追加してストアを設定することなく、実際のアプリケーションコードの記述にすぐに取り組むことができます。

プロジェクトにReduxを追加する方法の詳細については、この説明を参照してください

詳細な説明:ReactプロジェクトへのReduxの追加

CRA用の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')
)

初期プロジェクトの探索

この初期プロジェクトは、標準のCreate-React-Appプロジェクトテンプレートに基づいており、いくつかの変更が加えられています。

初期プロジェクトに含まれるものを簡単に見てみましょう

  • /src
    • index.js:アプリケーションのエントリーポイントファイル。メインの<App>コンポーネントをレンダリングします。
    • App.js:メインのアプリケーションコンポーネント。
    • index.css:アプリケーション全体のスタイル
    • /api
      • client.js:GETおよびPOSTリクエストを送信できる小さなAJAXリクエストクライアント
      • server.js:データの偽のREST APIを提供します。アプリは後でこれらの偽のエンドポイントからデータをフェッチします。
    • /exampleAddons:チュートリアルの後半で使用して、物事がどのように機能するかを示すいくつかの追加のReduxアドオンが含まれています

今すぐアプリをロードすると、ウェルカムメッセージが表示されますが、それ以外の場合、アプリの残りの部分は空です。

それでは始めましょう!

Todoサンプルアプリの開始

私たちのサンプルアプリケーションは、小さな「todo」アプリケーションになります。以前にtodoアプリの例を見たことがあるでしょう。これらは、項目のリストの追跡、ユーザー入力の処理、およびデータが変更されたときにUIを更新するなど、通常のアプリケーションで発生するすべてのことを示すことができるため、良い例になります。

要件の定義

このアプリケーションの最初のビジネス要件を調べてみましょう

  • UIは3つの主要なセクションで構成されます
    • ユーザーが新しいtodo項目のテキストを入力できる入力ボックス
    • 既存のすべてのtodo項目のリスト
    • 完了していないtodoの数を示し、フィルタリングオプションを表示するフッターセクション
  • Todoリスト項目には、その「完了」ステータスを切り替えるチェックボックスが必要です。また、事前定義された色のリストの色分けされたカテゴリタグを追加し、todo項目を削除できるようにする必要があります。
  • カウンターは、アクティブなtodoの数を複数形にする必要があります:「0項目」、「1項目」、「3項目」など
  • すべてのtodoを完了としてマークするボタンと、完了したすべてのtodoを削除してクリアするボタンが必要です
  • リストに表示されるtodoをフィルタリングする方法は2つある必要があります
    • 「すべて」、「アクティブ」、および「完了」todoを表示することに基づくフィルタリング
    • 1つ以上の色を選択し、その色と一致するタグを持つすべてのtodoを表示することに基づくフィルタリング

後でいくつかの要件を追加しますが、これで開始するのに十分です。

最終的な目標は、このようなアプリにすることです

Example todo app screenshot

ステート値の設計

ReactとReduxのコア原則の1つは、UIはステートに基づいている必要があるということです。したがって、アプリケーションを設計する1つのアプローチは、最初にアプリケーションがどのように機能するかを記述するために必要なすべてのステートを考えることです。また、状態にできるだけ少ない値でUIを記述しようとすることも良い考えです。これにより、追跡および更新する必要のあるデータが少なくなります。

概念的には、このアプリケーションには2つの主な側面があります

  • 現在のtodo項目の実際のリスト
  • 現在のフィルタリングオプション

また、「Todoを追加」の入力ボックスにユーザーが入力したデータも追跡する必要がありますが、それはそれほど重要ではないので、後で対応します。

各Todoアイテムについて、いくつかの情報を保存する必要があります。

  • ユーザーが入力したテキスト
  • 完了したかどうかを示すブール値フラグ
  • 一意のID値
  • 選択されている場合は、色のカテゴリ

フィルター処理の動作は、列挙型で記述できるでしょう。

  • 完了ステータス: 「すべて」、「アクティブ」、「完了」
  • 色: 「赤」、「黄」、「緑」、「青」、「オレンジ」、「紫」

これらの値をみると、Todoは「アプリの状態」(アプリケーションが扱うコアデータ)であり、フィルタリングの値は「UIの状態」(アプリが現在何をしているかを記述する状態)であると言うこともできます。これらの異なる種類のカテゴリについて考えると、異なる状態がどのように使用されているかを理解するのに役立ちます。

状態構造の設計

Reduxでは、アプリケーションの状態は常にプレーンなJavaScriptオブジェクトと配列で保持されます。つまり、Reduxの状態に他のものを入れることはできません。クラスインスタンス、Map / Set / Promise / Dateのような組み込みのJS型、関数、またはプレーンなJSデータではないものは何も入れられません。

ルートRedux状態の値は、ほとんどの場合、プレーンなJSオブジェクトであり、他のデータがその中にネストされています。

この情報に基づいて、Redux状態に必要な値の種類を記述できるようになるはずです。

  • まず、Todoアイテムオブジェクトの配列が必要です。各アイテムには、次のフィールドが必要です。
    • id: 一意の数値
    • text: ユーザーが入力したテキスト
    • completed: ブール値フラグ
    • color: オプションの色のカテゴリ
  • 次に、フィルタリングオプションを記述する必要があります。以下が必要です。
    • 現在の「完了」フィルター値
    • 現在選択されている色のカテゴリの配列

したがって、アプリの状態の例は次のようになります。

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の外に他の状態の値があっても問題ないことに注意してください。この例は今のところ十分に小さいため、実際にはすべての状態がReduxストアにありますが、後でわかるように、一部のデータはReduxに保持する必要はありません(「このドロップダウンは開いていますか?」や「フォーム入力の現在の値」など)。

アクションの設計

アクションは、typeフィールドを持つプレーンなJavaScriptオブジェクトです。前述したように、アクションはアプリケーションで発生したことを記述するイベントとして考えることができます

アプリの要件に基づいて状態構造を設計したのと同じように、何が起こっているかを記述するアクションのリストも作成できるはずです。

  • ユーザーが入力したテキストに基づいて新しいTodoエントリを追加する
  • Todoの完了ステータスを切り替える
  • Todoの色カテゴリを選択する
  • Todoを削除する
  • すべてのTodoを完了としてマークする
  • 完了したすべてのTodoをクリアする
  • 別の「完了」フィルター値を選択する
  • 新しいカラーフィルターを追加する
  • カラーフィルターを削除する

通常、何が起こっているかを記述するために必要な追加のデータは、action.payloadフィールドに入れます。これは、数値、文字列、または中に複数のフィールドを持つオブジェクトである可能性があります。

Reduxストアは、action.typeフィールドの実際のテキストを気にしません。ただし、自分のコードでは、更新が必要かどうかを確認するためにaction.typeを見ます。また、Redux DevTools拡張機能でデバッグ中にアクションタイプ文字列を見て、アプリで何が起こっているかを確認することがよくあります。したがって、読みやすく、何が起こっているかを明確に記述するアクションタイプを選択するようにしてください。後で見るときに物事を理解するのがはるかに簡単になります!

発生する可能性のあることのリストに基づいて、アプリケーションで使用するアクションのリストを作成できます。

  • {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つのアクションとして実行します。

状態データと同様に、アクションには何が起こったかを記述するために必要な最小限の情報を含める必要があります

リデューサーの作成

状態構造とアクションがどのように見えるかがわかったので、最初のリデューサーを作成する時が来ました。

リデューサーは、現在のstateactionを引数として受け取り、新しいstateの結果を返す関数です。言い換えれば、(state, action) => newState

ルートリデューサーの作成

Reduxアプリには、実際には1つのリデューサー関数、つまり後でcreateStoreに渡す「ルートリデューサー」関数しかありません。その1つのルートリデューサー関数は、ディスパッチされるすべてのアクションを処理し、毎回すべての新しい状態の結果がどうあるべきかを計算する役割を担います。

まず、srcフォルダーにreducer.jsファイルを作成しましょう。index.jsおよびApp.jsと一緒にです。

すべてのリデューサーには初期状態が必要なので、開始するためにいくつかの偽のTodoエントリを追加します。次に、リデューサー関数内のロジックの概要を記述できます。

src/reducer.js
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
}
}

リデューサーは、アプリケーションの初期化時に状態の値としてundefinedで呼び出される場合があります。その場合は、リデューサーコードの残りの部分が処理できるように、初期状態の値を提供する必要があります。リデューサーは通常、デフォルトの引数構文を使用して初期状態を提供します: (state = initialState, action)

次に、'todos/todoAdded'アクションを処理するロジックを追加しましょう。

まず、現在のアクションのタイプがその特定の文字列と一致するかどうかを確認する必要があります。次に、変更されなかったフィールドを含め、すべての状態を含む新しいオブジェクトを返す必要があります。

src/reducer.js
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
}
}

これは...状態に1つのTodoアイテムを追加するには大変な作業です。なぜこれほど余分な作業が必要なのでしょうか?

リデューサーのルール

先ほど、リデューサーは常にいくつかの特別なルールに従う必要があると述べました。

  • stateactionの引数に基づいて新しい状態の値のみを計算する必要があります
  • 既存のstateを変更することは許可されていません。代わりに、既存のstateをコピーし、コピーされた値を変更することで、イミュータブルな更新を行う必要があります。
  • 非同期ロジックやその他の「副作用」を実行してはなりません
ヒント

「副作用」とは、関数から値を返す以外の場所で確認できる状態または動作に対する変更です。一般的な副作用の例としては、次のようなものがあります。

  • コンソールへの値のロギング
  • ファイルの保存
  • 非同期タイマーの設定
  • AJAX HTTPリクエストの作成
  • 関数外に存在する状態の変更、または関数への引数の変更
  • 乱数または一意のランダムIDの生成 (Math.random()Date.now() など)

これらのルールに従うすべての関数は、リデューサー関数として特に記述されていなくても、「純粋な」関数としても知られています。

しかし、なぜこれらのルールが重要なのでしょうか?いくつかの異なる理由があります。

  • Reduxの目標の1つは、コードを予測可能にすることです。関数の出力が入力引数のみから計算される場合、そのコードがどのように機能するかを理解し、テストするのが容易になります。
  • 一方、関数がそれ自体以外の変数に依存する場合、またはランダムに動作する場合は、実行時に何が起こるかわかりません。
  • 関数がその引数を含む他の値を変更すると、アプリケーションの動作が予期せず変更される可能性があります。これは、「状態を更新したのに、UIが正しく更新されない!」などのバグの一般的な原因となる可能性があります。
  • Redux DevToolsの機能の一部は、リデューサーがこれらのルールに正しく従っていることに依存しています。

「イミュータブルな更新」に関するルールは特に重要であり、さらに詳しく説明する価値があります。

リデューサーとイミュータブルな更新

前に、「ミューテーション」(既存のオブジェクト/配列の値の変更)と「イミュータビリティ」(変更できないものとして値を扱うこと)について説明しました。

危険

Reduxでは、リデューサーが元の/現在の状態値を変更することは決して許可されていません!

// ❌ 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の配列/オブジェクトのスプレッド演算子と、元の値のコピーを返すその他の関数を使用して、手動でイミュータブルな更新を記述できることをすでに確認しました。

データがネストされている場合、これはより困難になります。イミュータブルな更新の重要なルールは、更新が必要なネストのすべてのレベルのコピーを作成する必要があるということです。

ただし、「このように手動でイミュータブルな更新を記述するのは、覚えるのも正しく行うのも難しいように見える」と思っているなら、そう、あなたは正しいです! :)

不変な更新ロジックを手動で記述するのは難しくリデューサーで誤って状態をミューテートさせることは、Reduxユーザーが犯す最も一般的な間違いです

ヒント

実際のアプリケーションでは、これらの複雑なネストされた不変な更新を手動で記述する必要はありませんパート8:Redux ToolkitによるモダンReduxでは、Redux Toolkitを使用して、リデューサーでの不変な更新ロジックの記述を簡素化する方法を学びます。

追加のアクションの処理

それを念頭に置いて、さらにいくつかの場合のリデューサーロジックを追加しましょう。まず、IDに基づいてToDoのcompletedフィールドを切り替えます。

src/reducer.js
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
}
}

そして、ToDoの状態に焦点を当ててきたので、「表示選択が変更された」アクションを処理するためのケースも追加しましょう。

src/reducer.js
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つのアクションしか処理していませんが、すでに少し長くなってきています。この1つのリデューサー関数ですべてのアクションを処理しようとすると、すべてを読むのが難しくなります。

そのため、リデューサーは通常、複数の小さなリデューサー関数に分割されます。リデューサーロジックをより理解しやすく、保守しやすくするためです。

リデューサーの分割

この一環として、Reduxリデューサーは通常、更新するRedux状態のセクションに基づいて分割されます。ToDoアプリの状態には現在、state.todosstate.filtersの2つのトップレベルのセクションがあります。したがって、大きなルートリデューサー関数を、2つの小さなリデューサー(todosReducerfiltersReducer)に分割できます。

では、これらの分割されたリデューサー関数はどこに配置すればよいでしょうか?

アプリケーションの特定の概念や領域に関連するコードである「機能」に基づいてReduxアプリのフォルダとファイルを整理することをお勧めします特定の機能のReduxコードは通常、「スライス」ファイルと呼ばれる1つのファイルとして記述され、そのアプリ状態の部分のリデューサーロジックとすべてのアクション関連コードが含まれています。

そのため、Reduxアプリ状態の特定セクションのリデューサーは「スライスリデューサー」と呼ばれます。通常、一部のアクションオブジェクトは特定のスライスリデューサーに密接に関連するため、アクションタイプの文字列は、その機能の名前('todos'など)で始まり、発生したイベント('todoAdded'など)を記述し、1つの文字列('todos/todoAdded')に結合する必要があります。

プロジェクトで、新しいfeaturesフォルダを作成し、その中にtodosフォルダを作成します。todosSlice.jsという名前の新しいファイルを作成し、ToDo関連の初期状態をこのファイルに切り取って貼り付けましょう。

src/features/todos/todosSlice.js
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関連の状態のみを更新する必要があり、これ以上ネストされていません!これは、リデューサーを分割するもう1つの理由です。ToDoの状態はそれ自体が配列であるため、ここで外側のルート状態オブジェクトをコピーする必要はありません。これにより、このリデューサーが読みやすくなります。

これはリデューサー合成と呼ばれ、Reduxアプリを構築する基本的なパターンです。

これらのアクションを処理した後、更新されたリデューサーは次のようになります。

src/features/todos/todosSlice.js
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を作成し、フィルター関連のコードをすべてそこに移動しましょう。

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つのルートリデューサー関数が必要であると先ほど述べました。では、1つの大きな関数にすべてのコードを記述せずに、どのようにルートリデューサーに戻ることができるでしょうか?

リデューサーは通常のJS関数であるため、スライスリデューサーをreducer.jsにインポートし、他の2つの関数を呼び出すことだけを仕事とする新しいルートリデューサーを記述できます。

src/reducer.js
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をインポートして使用できます。

src/reducer.js
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のような関数です。
    • リデューサーは常に特別なルールに従う必要があります。
      • stateaction引数に基づいてのみ、新しい状態を計算します。
      • 既存のstateをミューテートせずに、常にコピーを返します。
      • AJAX呼び出しや非同期ロジックなどの「副作用」はありません。
  • リデューサーは、読みやすくするために分割する必要があります。
    • リデューサーは通常、トップレベルの状態キーまたは状態の「スライス」に基づいて分割されます。
    • リデューサーは通常、「機能」フォルダに整理された「スライス」ファイルに記述されます。
    • リデューサーは、ReduxのcombineReducers関数で結合できます。
    • combineReducersに指定されたキー名によって、トップレベルの状態オブジェクトキーが定義されます。

次へ

これで、状態を更新するリデューサーロジックがいくつかできました。しかし、それらのリデューサーは単独では何も行いません。何かが起こったときにアクションを使用してリデューサーコードを呼び出すことができるReduxストア内に配置する必要があります。

パート4:ストアでは、Reduxストアを作成し、リデューサーロジックを実行する方法を見ていきます。