LoginSignup
3
0

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-06-20
[前回] Django+Reactで学ぶプログラミング基礎(32): Reduxチュートリアル(Todoアプリの設計と実装)

はじめに

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

今回の内容

Todoアプリの構築

  • 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

  • Reducerを分割するもう1つの理由は、reducer composition

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

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

  • 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

Reducerの結合

  • store作成には、1つのルートReducer関数が必須
    • 新しいルートReducerを作成し、上記で分割した2つのsliceReducer(通常の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

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

combineReducers

  • 新しいルートReducerの手動作成手順

    • slicereducerを呼び出し、そのreducerが管理するstateスライスを渡す
    • 結果をルートstateオブジェクトに割り当てる
    • さらにslicereducerを追加する場合、上記パターンを繰り返す
  • 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

おわりに

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

[次回] Django+Reactで学ぶプログラミング基礎(34): Reduxチュートリアル(TodoアプリのStore)
3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0