[前回] Django+Reactで学ぶプログラミング基礎(32): Reduxチュートリアル(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を複数の小さなreducer関数に分割
- reducerを、更新対象stateのセクションに基づいて分割
- Todoアプリのstateに2つのトップレベルセクションが存在
- state.todos
- todosReducer
- state.filters
- filtersReducer
- state.todos
- Todoアプリのstateに2つのトップレベルセクションが存在
- 分割されたreducer関数の配置場所
- Reduxアプリのフォルダーとファイルを、コードの機能(
feature
)に基づき整理 - 特定featureのReduxコードを、
slice
ファイルと呼ばれる単一ファイルに記述-
slice
ファイルには、アプリの特定stateを制御する、すべてのreducerロジックとaction処理が含まれる
-
- Reduxアプリのフォルダーとファイルを、コードの機能(
- 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
}
}
-
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
}
}
- UIのフィルター処理も同じく、
src/features/filters/filtersSlice.js
を新規作成- フィルター関連のaction処理を移動
-
state.filters
のようなネストが不要となり、コードが読みやすくなる
-
- フィルター関連のaction処理を移動
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
}
}
Reducerの結合
- store作成には、1つのルートReducer関数が必須
- 新しいルートReducerを作成し、上記で分割した2つの
slice
Reducer(通常のJS関数)を呼び出す(slice
ファイルをインポート)
- 新しいルートReducerを作成し、上記で分割した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)
}
}
- 各
slice
reducerは、独自のstateを管理- reducerに引数として渡されるstateは、自ずと管理するstateのみ
- これにより、機能(feature)とstateに基づき、ロジックを分割し、保守しやすくなる
- reducerに引数として渡されるstateは、自ずと管理する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スライスを更新
- combineReducersで、ルート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
おわりに
TodoアプリのReducerの設計と実装を行いました。
次回も続きます。お楽しみに。