[前回] Django+Reactで学ぶプログラミング基礎(31): Reduxチュートリアル(Todoアプリの要件定義と設計)
はじめに
前回は、Todoアプリの要件定義と設計を行いました。
今回は、設計の残り部分および実装に関してです。
今回の内容
- Actionの設計
- Reducerの実装
Actionの設計
-
actionは、typeフィールドを持つプレーンなJavaScriptオブジェクト
- アプリで何が発生したかを表すイベント
- 発生イベントに関する必要最小限の情報のみ含める
-
Todoアプリで対応すべき、actionリスト
- ユーザー入力のテキストに基づき、新しいTodoエントリを追加
- Todoを完了ステータスに切り替える
- Todoのカラーカテゴリを選択
- Todoを削除
- すべてのTodoに完了マークを付ける
- 完了したタスクをすべて削除
- 完了ステータスによるフィルタリング
- 新しいカラーフィルターを追加
- 既存のカラーフィルターを取り外す
-
actionのtypeフィールドは、actionの一意識別子として使用される
- actionを分かりやすく説明できる文字列を、typeフィールドにセット
-
actionのpayloadフィールドは、acitonの説明に必要な追加データを保存
- 数値/文字列/オブジェクト(内部に複数フィールドを持つ)などのデータ型を使用可
-
Todoアプリで発生可能イベントに基づき、actionリストを作成
- カラーフィルターの操作は、
追加
用と削除済み
用の2種類存在- 2つのaction.typeに分割してもよいが
- ここでは、payloadフィールドの
changeType
を用いて区別
- カラーフィルターの操作は、
{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}}
Reducerの実装
- Reducerは、現在のstateとactionを引数と取り、新しいstateの結果を返す関数
(state、action) => newState
ルートReducerの作成
- Reduxアプリには、1つのReducer関数
ルートReducer関数
しか存在しない- store作成の
createStore
メソッドに、引数としてルートReducer関数が渡される - ルートReducer関数は、dispatchされたすべてのactionを処理し、新しいstateの結果を計算する役割を果たす
- store作成の
まず、src
フォルダーにreducer.js
ファイルを作成
- すべてのreducerには初期状態が必要
- 初期値としてTodoエントリを三つ追加
- 次に、reducer関数内にロジック概要を記述
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: []
}
}
// initialStateをデフォルト値として使用
export default function appReducer(state = initialState, action) {
// reducerは通常actionのtypeフィールドの値により、何が起こったか判断
switch (action.type) {
// actionのtype別処理を行う
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
}
}
- reducerは、アプリ初期化時にstate値が
undefined
で呼び出される場合あり- 対処として、stateの初期値を指定し、残りのreducerコードが機能するように
- reducerは通常、ES6のデフォルトの引数構文を使用し、stateの初期値を提供
(state = initialState、action)
次に、todos/todoAdded
actionを処理するロジックを追加
- まず、カレントactionのtypeが特定文字列と一致するか確認
- 次に、変更されなかったフィールドについても、すべてのstateを含む新しいオブジェクトを返す必要あり
-
reduce()
メソッドは、配列の要素を一つずつ取り出し、指定した処理を行っていき、最終的に一つの値を返す関数-
todos.reduce()
は、初期値が-1のmaxId変数を、todos配列の要素と一つずつ比較し、最終的に配列要素の最大値を返す
-
-
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
}
}
Reducerのルール
-
既存stateの直接変更は許可されない
- 代わりに、既存のstateをコピーし、コピーされた値に変更を加えることで、イミュータブルな更新を行う
- 非同期ロジックやその他
副作用
を伴う処理を実行してはいけない-
副作用
とは、stateを変更する、またはstate値の返却以外の下記のような処理を指す- コンソールに値を記録
- ファイル保存
- 非同期タイマーの設定
- AJAX HTTPリクエストを作成
- 関数外部に存在するstateを変更、または関数の引数を書き換える
- 乱数または一意のランダムID(
Math.random()
やDate.now()
など)の生成
-
-
上記ルールが必要な理由
- Reduxの目標の1つは、コードを予測可能にすること
- 関数の出力が入力引数からのみ計算されると、そのコードがどのように機能するか理解し、テストも簡単になる
- 関数が外部の変数に依存する、またはランダムに動作する場合
- 関数を実行したときに何が起こるか予測不能
- 関数が引数や他の値を変更した場合
- アプリが予期しない動作をする可能性あり
-
stateを更新したが、UIが更新されない
などバグの元となる
-
- アプリが予期しない動作をする可能性あり
- Redux DevToolsの機能の一部は、reducerがこれらのルールに正しく従うことを前提とする
- Reduxの目標の1つは、コードを予測可能にすること
Reducerとイミュータブルな更新
- Reduxでは、reducerが元のstate値を直接変更することを許可しない
- 許可されないstate変更例
state.value = 123
- 許可されないstate変更例
- Reduxでstateを直接変更してはならない理由
- UIが正しく更新されない、最新の値が表示されない、などバグ発生の可能性あり
- stateが更新された理由と方法の追跡が難しくなる
- テスト作成が難しくなる
-
タイムトラベルデバッグ
が正しく機能しなくなる
- 元のstateを変更せず、更新されたstateを返却する方法
- reducerは、元のstateのコピーを作成し、そのコピーを変更
- スプレッド演算子を用いて、安全にstateを変更する例
return { ...state, value: 123}
- スプレッド演算子を用いて、安全にstateを変更する例
- データがネストされている場合、イミュータブルな更新を行う方法
- 更新が必要なネストレベルに対しても、データコピーを作成する必要あり
- 実際のアプリでは、Redux Toolkitを使用し、複雑なネストに対しイミュータブルな更新を実施(手動でコピー作成する必要なし)
- reducerは、元のstateのコピーを作成し、そのコピーを変更
おわりに
Reduxを用いた、Todoアプリの設計と実装を行いました。
次回も続きます。お楽しみに。