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

##### [前回] [Django+Reactで学ぶプログラミング基礎(32): Reduxチュートリアル(Todoアプリの設計と実装)](https://qiita.com/mingchun_zhao/items/e83a812204966f6d4a83)

## はじめに

前回は、Todoアプリの設計と実装を行いました。
今回は、その続きです。

## 今回の内容

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

- ReducerにAction処理を追加
- Reducerを分割
- Reducerを結合

## ReducerにAction処理を追加

- まず、`todo.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
  }
}
```

- 次に、UIでフィルター選択が変わる際のaction処理を追加

```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
  }
}
```

## Reducerを分割

- actionの数が増えるにつれコードが長くなり、1つのreducer関数では可読性が低下
    - reducerを複数の小さなreducer関数に分割
        - ロジックの理解と保守をしやすくなる
- reducerを、更新対象stateのセクションに基づいて分割
    - Todoアプリのstateに2つのトップレベルセクションが存在
        - state.todos
            - todosReducer
        - state.filters
            - filtersReducer
- 分割されたreducer関数の配置場所
    - Reduxアプリのフォルダーとファイルを、コードの機能(`feature`)に基づき整理
    - 特定featureのReduxコードを、`slice`ファイルと呼ばれる単一ファイルに記述
        - `slice`ファイルには、アプリの特定stateを制御する、すべてのreducerロジックとaction処理が含まれる
- Reduxアプリの特定stateセクションのreducerを`slice reducer`と呼ぶ
    - actionオブジェクトは特定のslice reducerと密接に関連する
    - actionのtype命名(文字列)は、機能(`todos`など)と発生イベント(`todoAdded`など)両方を説明する必要あり
        - 例: `todos/todoAdded`
- このプロジェクトでは、新しい`features`フォルダーを作成
    - その配下に`todos`フォルダーを作成
        - `todosSlice.js`ファイルを追加し、Todo関連stateの初期値をカット/ペースト
```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
  }
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/a22e1e53-daee-07fe-03cc-12a1b07119cf.png)

- Reducerを分割するもう1つの理由は、`reducer composition`
    - `reducer composition`は、Reduxアプリを構築するための基本的なパターン
    - `todosSlice.js`ファイルは、`todos`関連のstateを更新するだけで済む
        - `state.todos`のようなネストは不要
    - `todos`stateの実態は配列で、外部ルートstateオブジェクトをコピー不要
        - これにより、reducerが読みやすくなる

- action処理を追加後の`todosSlice`reducer
```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
  }
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/595dac88-402d-403e-5800-f17c50b798c8.png)

- UIのフィルター処理も同じく、`src/features/filters/filtersSlice.js`を新規作成
    - フィルター関連のaction処理を移動
        - `state.filters`のようなネストが不要となり、コードが読みやすくなる
```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
  }
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/a1540310-6ca2-8f3d-4d16-affb57575555.png)

## Reducerの結合

- store作成には、1つのルートReducer関数が必須
    - 新しいルートReducerを作成し、上記で分割した2つの`slice`Reducer(通常のJS関数)を呼び出す(`slice`ファイルをインポート)
```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)
  }
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/1cc7ef7d-ad70-5e2c-5019-54ddfe561e9f.png)

- 各`slice`reducerは、独自のstateを管理
    - reducerに引数として渡されるstateは、自ずと管理するstateのみ
        - これにより、機能(feature)とstateに基づき、ロジックを分割し、保守しやすくなる

### combineReducers

- 新しいルートReducerの手動作成手順
    - `slice`reducerを呼び出し、そのreducerが管理するstateスライスを渡す
    - 結果をルートstateオブジェクトに割り当てる
    - さらに`slice`reducerを追加する場合、上記パターンを繰り返す

- Reduxコアライブラリには、combineReducersユーティリティが含まれている
    - 上記ルートReducerの手動作成を、combineReducersを用いて自動生成可能

- combineReducersを使用するには、Reduxコアライブラリをインストール(未インストールの場合)
```
npm install redux
```

- combineReducersをインポート
    - combineReducersで、ルートstateオブジェクト
        - Key: 各slice reducerで管理するstateオブジェクトのキーと一致
        - Value: slice recuder関数で、それぞれ管理するstateスライスを更新
```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
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/0e4ed313-ea57-b9ed-73a8-19988184faf6.png)

## おわりに

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

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