Django+Reactで学ぶプログラミング基礎(32): Reduxチュートリアル(Todoアプリの設計と実装)

##### [前回] [Django+Reactで学ぶプログラミング基礎(31): Reduxチュートリアル(Todoアプリの要件定義と設計)](https://qiita.com/mingchun_zhao/items/9f379da807058a3d42f5)

## はじめに

前回は、Todoアプリの要件定義と設計を行いました。
今回は、設計の残り部分および実装に関してです。

## 今回の内容

[Todoアプリの構築](https://redux.js.org/tutorials/fundamentals/part-3-state-actions-reducers#designing-actions)

- 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の結果を計算する役割を果たす

#### まず、`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
  }
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/98275666-5b71-c82c-aaac-9ec05f1a88b0.png)

- 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がこれらのルールに正しく従うことを前提とする

### Reducerとイミュータブルな更新

- Reduxでは、reducerが元のstate値を直接変更することを許可しない
    - 許可されないstate変更例
        - `state.value = 123`
- Reduxでstateを直接変更してはならない理由
    - UIが正しく更新されない、最新の値が表示されない、などバグ発生の可能性あり
    - stateが更新された理由と方法の追跡が難しくなる
    - テスト作成が難しくなる
    - `タイムトラベルデバッグ`が正しく機能しなくなる
- 元のstateを変更せず、更新されたstateを返却する方法
    - reducerは、元のstateのコピーを作成し、そのコピーを変更
        - スプレッド演算子を用いて、安全にstateを変更する例
            - `return { ...state, value: 123}`
    - データがネストされている場合、イミュータブルな更新を行う方法
        - 更新が必要なネストレベルに対しても、データコピーを作成する必要あり
        - 実際のアプリでは、Redux Toolkitを使用し、複雑なネストに対しイミュータブルな更新を実施(手動でコピー作成する必要なし)

## おわりに

Reduxを用いた、Todoアプリの設計と実装を行いました。
次回も続きます。お楽しみに。

##### [次回] [Django+Reactで学ぶプログラミング基礎(33): Reduxチュートリアル(Todoアプリの実装(続き))](https://qiita.com/mingchun_zhao/items/dbfab155c091b7c87f8b)