Redux Fundamentals, Part 7: 標準的なReduxパターン
このページは PageTurner AI で翻訳されました(ベータ版)。プロジェクト公式の承認はありません。 エラーを見つけましたか? 問題を報告 →
- 実際のReduxアプリで使用される標準パターンとその存在理由:
- アクションオブジェクトをカプセル化するアクションクリエーター
- パフォーマンス向上のためのメモ化セレクター
- ローディング列挙型によるリクエスト状態の追跡
- アイテムコレクション管理のための状態正規化
- プロミスとサンクの操作
- これまでの全セクションの内容の理解
パート6:非同期ロジックとデータ取得では、Reduxミドルウェアを使用してストアと通信する非同期ロジックの記述方法を学びました。特にRedux "thunk"ミドルウェアを使用し、どのReduxストアと通信するか事前に知らなくても再利用可能な非同期ロジックを含む関数を記述しました。
これまでにReduxの動作の基本をカバーしました。しかし実際のReduxアプリケーションでは、これらの基本の上に追加のパターンを使用します。
重要な点として、これらのパターンはReduxを使用するために必須ではありません! しかし、各パターンが存在する十分な理由があり、ほぼすべてのReduxコードベースでこれらの一部または全てを見ることになるでしょう。
このセクションでは、既存のTodoアプリのコードをこれらのパターンを使用するように改修し、なぜReduxアプリで一般的に使用されるのかについて説明します。その後パート8では、「モダンなRedux」について、公式のRedux Toolkitパッケージを使用してアプリ内で「手動で」記述したReduxロジックをすべて簡素化する方法と、Reduxアプリを記述する標準的なアプローチとしてRedux Toolkitの使用を推奨する理由について説明します。
このページは PageTurner AI で翻訳されました(ベータ版)。プロジェクト公式の承認はありません。 エラーを見つけましたか? 問題を報告 →
このチュートリアルでは、Reduxの基本原則と概念を説明するために、あえて旧来のスタイルのReduxロジックパターンを使用しています。これらは、現代的なReduxアプリ開発の正しいアプローチとして推奨しているRedux Toolkitを使った「モダンRedux」パターンに比べてコード量が多くなります。このチュートリアルは_プロダクション環境で使用することを想定したものではありません_。
「モダンRedux」をRedux Toolkitで実践する方法については、以下のページを参照してください:
- 完全版「Redux Essentials」チュートリアル: 現実世界のアプリケーションでRedux Toolkitを使った「Reduxの正しい使い方」を解説しています。すべてのRedux学習者は「Essentials」チュートリアルの読了を推奨します!
- Redux Fundamentals パート8: Redux ToolkitによるモダンRedux: 前半セクションのローレベルな例をモダンなRedux Toolkitの実装に変換する方法を解説
アクションクリエーター
これまでアプリでは、ディスパッチされる場所のコード内で直接アクションオブジェクトを記述してきました:
dispatch({ type: 'todos/todoAdded', payload: trimmedText })
しかし実際には、よく書かれたReduxアプリでは、ディスパッチ時にインラインでこれらのアクションオブジェクトを記述しません。代わりに「アクションクリエーター」関数を使用します。
アクションクリエーター
const todoAdded = text => {
return {
type: 'todos/todoAdded',
payload: text
}
}
使用方法は、アクションクリエーターを呼び出し、その結果のアクションオブジェクトを直接dispatchに渡します:
store.dispatch(todoAdded('Buy milk'))
console.log(store.getState().todos)
// [ {id: 0, text: 'Buy milk', completed: false}]
Detailed Explanation: Why use Action Creators?
In our small example todo app, writing action objects by hand every time isn't too difficult. In fact, by switching to using action creators, we've added more work - now we have to write a function and the action object.
But, what if we needed to dispatch the same action from many parts of the application? Or what if there's some additional logic that we have to do every time we dispatch an action, like creating a unique ID? We'd end up having to copy-paste the additional setup logic every time we need to dispatch that action.
Action creators have two primary purposes:
- They prepare and format the contents of action objects
- They encapsulate any additional work needed whenever we create those actions
That way, we have a consistent approach for creating actions, whether or not there's any extra work that needs to be done. The same goes for thunks as well.
アクションクリエーターの使用
いくつかのアクションタイプに対して、todosスライスファイルをアクションクリエーターを使用するように更新しましょう。
これまで使用してきた2つの主要なアクションから始めます:サーバーからTodoリストを読み込むアクションと、サーバーに保存した後に新しいTodoを追加するアクションです。
現在、todosSlice.jsは次のように直接アクションオブジェクトをディスパッチしています:
dispatch({ type: 'todos/todosLoaded', payload: response.todos })
同じ種類のアクションオブジェクトを作成して返す関数を作成しますが、引数としてTodoの配列を受け取り、それをaction.payloadとしてアクションに配置します。その後、fetchTodosサンク内で新しいアクションクリエーターを使用してアクションをディスパッチできます:
export const todosLoaded = todos => {
return {
type: 'todos/todosLoaded',
payload: todos
}
}
export async function fetchTodos(dispatch, getState) {
const response = await client.get('/fakeApi/todos')
dispatch(todosLoaded(response.todos))
}
「todo added」アクションについても同じことができます:
export const todoAdded = todo => {
return {
type: 'todos/todoAdded',
payload: todo
}
}
export function saveNewTodo(text) {
return async function saveNewTodoThunk(dispatch, getState) {
const initialTodo = { text }
const response = await client.post('/fakeApi/todos', { todo: initialTodo })
dispatch(todoAdded(response.todo))
}
}
ついでに、「color filter changed」アクションについても同じことをしましょう:
export const colorFilterChanged = (color, changeType) => {
return {
type: 'filters/colorFilterChanged',
payload: { color, changeType }
}
}
このアクションは<Footer>コンポーネントからディスパッチされていたため、colorFilterChangedアクションクリエーターをインポートして使用する必要があります:
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { availableColors, capitalize } from '../filters/colors'
import { StatusFilters, colorFilterChanged } from '../filters/filtersSlice'
// omit child components
const Footer = () => {
const dispatch = useDispatch()
const todosRemaining = useSelector(state => {
const uncompletedTodos = state.todos.filter(todo => !todo.completed)
return uncompletedTodos.length
})
const { status, colors } = useSelector(state => state.filters)
const onMarkCompletedClicked = () => dispatch({ type: 'todos/allCompleted' })
const onClearCompletedClicked = () =>
dispatch({ type: 'todos/completedCleared' })
const onColorChange = (color, changeType) =>
dispatch(colorFilterChanged(color, changeType))
const onStatusChange = status =>
dispatch({ type: 'filters/statusFilterChanged', payload: status })
// omit rendering output
}
export default Footer
colorFilterChangedアクションクリエーターは実際には2つの異なる引数を受け取り、それらを組み合わせて適切なaction.payloadフィールドを形成することに注意してください。
これはアプリケーションの動作やReduxのデータフローに何ら変更を与えません。依然としてアクションオブジェクトを作成しディスパッチしています。しかしコード内で常に直接アクションオブジェクトを記述する代わりに、ディスパッチ前にこれらのアクションオブジェクトを準備するためにアクションクリエーターを使用するようになりました。
アクションクリエーターはサンク関数と組み合わせて使用することもできます。実際、前のセクションではサンクをアクションクリエーターでラップしました。具体的には、saveNewTodoを「サンクアクションクリエーター」関数でラップし、textパラメータを渡せるようにしました。fetchTodosはパラメータを受け取りませんが、同様にアクションクリエーターでラップすることも可能です:
export function fetchTodos() {
return async function fetchTodosThunk(dispatch, getState) {
const response = await client.get('/fakeApi/todos')
dispatch(todosLoaded(response.todos))
}
}
つまり、index.js内でディスパッチしている箇所を変更し、外側のサンクアクションクリエーター関数を呼び出し、返された内側のサンク関数をdispatchに渡す必要があります:
import store from './store'
import { fetchTodos } from './features/todos/todosSlice'
store.dispatch(fetchTodos())
これまでサンクはfunctionキーワードを使って記述し、その動作を明確にしてきました。ただし、代わりにアロー関数構文を使用することも可能です。暗黙の返り値を使用するとコードを短縮できますが、アロー関数に慣れていない場合は可読性が低下する可能性もあります:
// Same thing as the above example!
export const fetchTodos = () => async dispatch => {
const response = await client.get('/fakeApi/todos')
dispatch(todosLoaded(response.todos))
}
同様に、必要に応じて通常のアクションクリエーターも短縮できます:
export const todoAdded = todo => ({ type: 'todos/todoAdded', payload: todo })
この方法でアロー関数を使用するかどうかは、あなた次第です。
アクションクリエーターが有用な理由の詳細については、以下を参照してください:
メモ化セレクター
Redux stateオブジェクトを引数として受け取り値を返す「セレクター」関数を作成できることは既に見てきました:
const selectTodos = state => state.todos
データを_導出_する必要がある場合はどうでしょうか?例えば、todoのIDだけを含む配列が欲しい場合:
const selectTodoIds = state => state.todos.map(todo => todo.id)
しかし、array.map()は常に新しい配列参照を返します。React-ReduxのuseSelectorフックは_すべての_ディスパッチされたアクション後にセレクター関数を再実行し、セレクター結果が変更された場合にコンポーネントの再レンダリングを強制します。
この例では、useSelector(selectTodoIds)を呼び出すと、新しい配列参照を返すため_すべての_アクション後にコンポーネントが再レンダリングされます!
パート5では、useSelectorの引数としてshallowEqualを渡せることを見ました。ただし、別の選択肢として「メモ化」セレクターを使用する方法もあります。
メモ化は一種のキャッシュ戦略です。具体的には、高コストな計算結果を保存し、後で同じ入力が与えられた場合にその結果を再利用します。
メモ化セレクター関数は最新の結果値を保存しておき、同じ入力で複数回呼び出された場合に同じ結果値を返します。前回と_異なる_入力で呼び出された場合は、新しい結果値を再計算し、キャッシュしてから新しい結果を返します。
createSelectorによるメモ化セレクター
Reselectライブラリはメモ化セレクター関数を生成するcreateSelector APIを提供します。createSelectorは1つ以上の「入力セレクター」関数と「出力セレクター」を引数として受け取り、新しいセレクター関数を返します。セレクターを呼び出すたびに:
-
すべての引数を使ってすべての「入力セレクター」が呼び出される
-
入力セレクターの戻り値のいずれかが変更された場合、「出力セレクター」が再実行される
-
入力セレクターの結果はすべて出力セレクターへの引数となる
-
出力セレクターの最終結果が次回のためにキャッシュされる
selectTodoIdsのメモ化バージョンを作成し、<TodoList>で使用してみましょう。
まず、Reselectをインストールする必要があります:
npm install reselect
次に、createSelectorをインポートして呼び出します。もともとのselectTodoIds関数はTodoList.jsで定義されていましたが、セレクター関数は関連するスライスファイルに記述するのが一般的です。それでは、これをtodosスライスに追加しましょう:
import { createSelector } from 'reselect'
// omit reducer
// omit action creators
export const selectTodoIds = createSelector(
// First, pass one or more "input selector" functions:
state => state.todos,
// Then, an "output selector" that receives all the input results as arguments
// and returns a final result value
todos => todos.map(todo => todo.id)
)
次に、<TodoList>でこれを使用しましょう:
import React from 'react'
import { useSelector, shallowEqual } from 'react-redux'
import { selectTodoIds } from './todosSlice'
import TodoListItem from './TodoListItem'
const TodoList = () => {
const todoIds = useSelector(selectTodoIds)
const renderedListItems = todoIds.map(todoId => {
return <TodoListItem key={todoId} id={todoId} />
})
return <ul className="todo-list">{renderedListItems}</ul>
}
これは実際にはshallowEqual比較関数とは少し異なる動作をします。state.todos配列が変更されるたびに、新しいtodoのID配列が生成されます。これには、completedフィールドのトグルといったtodoアイテムへの不変更新も含まれます。なぜなら、不変更新のために新しい配列を作成する必要があるからです。
メモ化されたセレクターは、元のデータから実際に追加の値を導出する場合にのみ有用です。既存の値を参照して返すだけの場合は、セレクターをプレーンな関数のままにしておくことができます。
複数引数を持つセレクター
私たちのtodoアプリは、完了ステータスに基づいて表示するtodoをフィルタリングする機能を持つことになっています。フィルタリングされたtodoリストを返すメモ化セレクターを作成しましょう。
出力セレクターの引数の1つとして、todos配列全体が必要であることがわかります。また、現在の完了ステータスのフィルター値も渡す必要があります。それぞれの値を抽出するための個別の「入力セレクター」を追加し、その結果を「出力セレクター」に渡します。
import { createSelector } from 'reselect'
import { StatusFilters } from '../filters/filtersSlice'
// omit other code
export const selectFilteredTodos = createSelector(
// First input selector: all todos
state => state.todos,
// Second input selector: current status filter
state => state.filters.status,
// Output selector: receives both values
(todos, status) => {
if (status === StatusFilters.All) {
return todos
}
const completedStatus = status === StatusFilters.Completed
// Return either active or completed todos based on filter
return todos.filter(todo => todo.completed === completedStatus)
}
)
2つのスライス間にインポート依存関係を追加したことに注意してください — todosSliceがfiltersSliceから値をインポートしています。これは問題ありませんが、注意が必要です。2つのスライスが互いに何かをインポートしようとすると、「循環インポート依存」の問題が発生し、コードがクラッシュする可能性があります。そのような場合は、共通コードを独自のファイルに移動し、そのファイルからインポートするようにしてください。
これで、この新しい「フィルタリングされたtodos」セレクターを、それらのtodoのIDを返す別のセレクターへの入力として使用できます:
export const selectFilteredTodoIds = createSelector(
// Pass our other memoized selector as an input
selectFilteredTodos,
// And derive data in the output selector
filteredTodos => filteredTodos.map(todo => todo.id)
)
<TodoList>でselectFilteredTodoIdsを使用するように変更すれば、いくつかのtodoアイテムを完了としてマークできるようになります:

そして、リストをフィルタリングして完了したtodo_のみ_を表示します:

次に、selectFilteredTodosを拡張して、選択に色フィルタリングも含めましょう:
export const selectFilteredTodos = createSelector(
// First input selector: all todos
selectTodos,
// Second input selector: all filter values
state => state.filters,
// Output selector: receives both values
(todos, filters) => {
const { status, colors } = filters
const showAllCompletions = status === StatusFilters.All
if (showAllCompletions && colors.length === 0) {
return todos
}
const completedStatus = status === StatusFilters.Completed
// Return either active or completed todos based on filter
return todos.filter(todo => {
const statusMatches =
showAllCompletions || todo.completed === completedStatus
const colorMatches = colors.length === 0 || colors.includes(todo.color)
return statusMatches && colorMatches
})
}
)
このセレクターにロジックをカプセル化することで、フィルタリングの動作を変更してもコンポーネントを変更する必要がなかったことに注目してください。これで、ステータスと色の両方で同時にフィルタリングできるようになりました:

最後に、コード内のいくつかの場所でstate.todosを参照しています。このセクションの残りの部分で状態の設計方法を変更する予定なので、単一のselectTodosセレクターを抽出し、どこでもそれを使用するようにします。また、selectTodoByIdをtodosSliceに移動することもできます:
export const selectTodos = state => state.todos
export const selectTodoById = (state, todoId) => {
return selectTodos(state).find(todo => todo.id === todoId)
}
セレクター関数を使用する理由やReselectを使ったメモ化セレクターの書き方の詳細については以下を参照してください:
非同期リクエストのステータス
サーバーから初期のtodoリストを取得するために非同期サンクを使用しています。フェイクサーバーAPIを使用しているため、レスポンスは即座に返ってきます。実際のアプリでは、API呼び出しの解決に時間がかかる場合があります。そのような場合、レスポンスが完了するのを待つ間、何らかのローディングスピナーを表示するのが一般的です。
これは通常、Reduxアプリでは以下のように処理されます:
-
リクエストの現在のステータスを示す何らかの「ローディング状態」の値を用意する
-
API呼び出しの_前に_「リクエスト開始」アクションをディスパッチし、ローディング状態の値を変更して処理する
-
リクエストが完了したときにローディング状態の値を再度更新して、呼び出しが完了したことを示す
UIレイヤーはリクエスト処理中にローディングスピナーを表示し、リクエスト完了時には実際のデータを表示するように切り替えます。
todosスライスを更新してローディング状態の値を追跡し、fetchTodosサンクの一部として追加の'todos/todosLoading'アクションをディスパッチします。
現在、todosリデューサーのstateは単なるtodoの配列です。todosスライス内でローディング状態を追跡するには、todosの状態をtodo配列とローディング状態値の両方を持つオブジェクトに再構成する必要があります。これにより、追加のネストを処理するためにリデューサーのロジックを書き換えることになります:
const initialState = {
status: 'idle',
entities: []
}
export default function todosReducer(state = initialState, action) {
switch (action.type) {
case 'todos/todoAdded': {
return {
...state,
entities: [...state.entities, action.payload]
}
}
case 'todos/todoToggled': {
return {
...state,
entities: state.entities.map(todo => {
if (todo.id !== action.payload) {
return todo
}
return {
...todo,
completed: !todo.completed
}
})
}
}
// omit other cases
default:
return state
}
}
// omit action creators
export const selectTodos = state => state.todos.entities
ここで注意すべき重要な点がいくつかあります:
-
todos配列は
todosReducerの状態オブジェクト内でstate.entitiesとしてネストされるようになりました。"entities"(エンティティ)という用語は「IDを持つユニークなアイテム」を意味し、これはtodoオブジェクトを的確に表現しています。 -
これは、配列がRedux全体の状態オブジェクト内で
state.todos.entitiesとしてネストされていることも意味します -
これでリデューサー内で追加のネストレベルを正しく不変更新するための追加手順(例:
stateオブジェクト →entities配列 →todoオブジェクト)が必要になりました -
コードの他の部分はセレクターを介してのみtodos状態にアクセスしているため、
selectTodosセレクターを更新するだけで済みます - 状態の構造を大幅に変更しても、UIの他の部分は期待通り動作し続けます
ローディング状態の列挙型(Enum)値
ローディング状態のフィールドを文字列の列挙型(enum)として定義していることにもお気づきでしょう:
{
status: 'idle' // or: 'loading', 'succeeded', 'failed'
}
isLoadingブール値の代わりにです。
ブール値では「ローディング中」または「ローディング中でない」の2つの可能性に制限されます。現実には、リクエストは実際には多数の異なる状態を取り得ます。例えば:
-
全く開始されていない
-
進行中
-
成功
-
失敗
-
成功したが、再取得が必要な状況に戻った
また、アプリのロジックでは特定のアクションに基づいてのみ特定の状態間を遷移させるべき場合があり、これはブール値を使用すると実装が難しくなります。
このため、ローディング状態はブール値フラグではなく文字列の列挙型(enum)値として保存することを推奨します。
ローディング状態を列挙型(enum)にする理由の詳細な説明については以下を参照してください:
これに基づき、ステータスを'loading'に設定する新しい「loading」アクションを追加し、「loaded」アクションを更新して状態フラグを'idle'にリセットします:
const initialState = {
status: 'idle',
entities: []
}
export default function todosReducer(state = initialState, action) {
switch (action.type) {
// omit other cases
case 'todos/todosLoading': {
return {
...state,
status: 'loading'
}
}
case 'todos/todosLoaded': {
return {
...state,
status: 'idle',
entities: action.payload
}
}
default:
return state
}
}
// omit action creators
// Thunk function
export const fetchTodos = () => async dispatch => {
dispatch(todosLoading())
const response = await client.get('/fakeApi/todos')
dispatch(todosLoaded(response.todos))
}
ただし、これをUIに表示する前に、フェイクサーバーAPIを修正してAPI呼び出しに人為的な遅延を追加する必要があります。src/api/server.jsを開き、63行目付近にある以下のコメントアウトされた行を探してください:
new Server({
routes() {
this.namespace = 'fakeApi'
// this.timing = 2000
// omit other code
}
})
この行のコメントを解除すると、フェイクサーバーはアプリが行うすべてのAPI呼び出しに2秒の遅延を追加し、ローディングスピナーが表示されるのを実際に確認するのに十分な時間を確保できます。
これで、<TodoList>コンポーネントでローディング状態の値を読み取り、その値に基づいてローディングスピナーを表示できます。
// omit imports
const TodoList = () => {
const todoIds = useSelector(selectFilteredTodoIds)
const loadingStatus = useSelector(state => state.todos.status)
if (loadingStatus === 'loading') {
return (
<div className="todo-list">
<div className="loader" />
</div>
)
}
const renderedListItems = todoIds.map(todoId => {
return <TodoListItem key={todoId} id={todoId} />
})
return <ul className="todo-list">{renderedListItems}</ul>
}
実際のアプリでは、APIの失敗エラーやその他の潜在的なケースも処理する必要があります。
ローディングステータスを有効にしたアプリの外観は以下の通りです(スピナーを再度表示するには、アプリプレビューをリロードするか新しいタブで開いてください):
Flux標準アクション(Flux Standard Actions)
Reduxストア自体はアクションオブジェクトにどのフィールドを入れるかは実際には気にしません。重要なのはaction.typeが存在し文字列であることだけです。つまり「todo追加」アクションにaction.todoを入れたり、action.colorを入れたり、自由に追加できるということです。
しかし、各アクションがデータフィールドに異なる名前を使うと、各リデューサーでどのフィールドを処理する必要があるか事前に把握するのが困難になります。
そのためReduxコミュニティは「Flux Standard Actions」規約(FSA)を考案しました。これはアクションオブジェクト内のフィールド整理方法を提案するもので、開発者が常にどのフィールドにどんなデータが含まれるか理解できるようにします。FSAパターンはReduxコミュニティで広く採用されており、実際にこのチュートリアル全体で既に使用しています。
FSA規約では以下を定めています:
-
アクションオブジェクトに実際のデータがある場合、その「データ」値は常に
action.payloadに入れる -
追加の記述データを
action.metaフィールドに入れてもよい -
エラー情報を
action.errorフィールドに入れてもよい
したがって、すべてのReduxアクションは必ず:
-
プレーンなJavaScriptオブジェクトであること
-
typeフィールドを持つこと
FSAパターンに従う場合、アクションは以下のフィールドを追加できます:
-
payloadフィールド -
errorフィールド -
metaフィールド
Detailed Explanation: FSAs and Errors
The FSA specification says that:
The optional
errorproperty MAY be set totrueif the action represents an error. An action whoseerroris true is analogous to a rejected Promise. By convention, thepayloadSHOULD be an error object. Iferrorhas any other value besidestrue, includingundefinedandnull, the action MUST NOT be interpreted as an error.
The FSA specs also argue against having specific action types for things like "loading succeeded" and "loading failed".
However, in practice, the Redux community has ignored the idea of using action.error as a boolean flag, and instead settled on separate action types, like 'todos/todosLoadingSucceeded' and 'todos/todosLoadingFailed'. This is because it's much easier to check for those action types than it is to first handle 'todos/todosLoaded' and then check if (action.error).
You can do whichever approach works better for you, but most apps use separate action types for success and failure.
正規化された状態
これまでtodosは配列で保持してきました。これはサーバーから配列としてデータを受け取り、UIでリストとして表示するためにループ処理が必要なため合理的です。
ただし、大規模なReduxアプリではデータを正規化された状態構造で保持するのが一般的です。「正規化」とは以下を意味します:
-
各データのコピーを1つだけ保持する
-
IDで直接アイテムを検索可能な方法で保存する
-
アイテム全体をコピーせず、IDで他のアイテムを参照する
例えばブログアプリでは、UserやCommentオブジェクトを参照するPostオブジェクトがあるとします。同じ人物による投稿が複数ある場合、全てのPostオブジェクトに完全なUserオブジェクトが含まれていると、同じUserオブジェクトのコピーが多数作成されます。代わりに、Postオブジェクトはpost.userとしてユーザーID値を持ち、state.users[post.user]のようにIDでUserオブジェクトを検索します。
つまり通常、データを配列ではなくオブジェクトとして整理します。アイテムIDをキー、アイテム自体を値として以下のように保持します:
const rootState = {
todos: {
status: 'idle',
entities: {
2: { id: 2, text: 'Buy milk', completed: false },
7: { id: 7, text: 'Clean room', completed: true }
}
}
}
todosスライスを正規化形式に変換しましょう。これにはリデューサーロジックの大幅な変更とセレクターの更新が必要です:
const initialState = {
status: 'idle',
entities: {}
}
export default function todosReducer(state = initialState, action) {
switch (action.type) {
case 'todos/todoAdded': {
const todo = action.payload
return {
...state,
entities: {
...state.entities,
[todo.id]: todo
}
}
}
case 'todos/todoToggled': {
const todoId = action.payload
const todo = state.entities[todoId]
return {
...state,
entities: {
...state.entities,
[todoId]: {
...todo,
completed: !todo.completed
}
}
}
}
case 'todos/colorSelected': {
const { color, todoId } = action.payload
const todo = state.entities[todoId]
return {
...state,
entities: {
...state.entities,
[todoId]: {
...todo,
color
}
}
}
}
case 'todos/todoDeleted': {
const newEntities = { ...state.entities }
delete newEntities[action.payload]
return {
...state,
entities: newEntities
}
}
case 'todos/allCompleted': {
const newEntities = { ...state.entities }
Object.values(newEntities).forEach(todo => {
newEntities[todo.id] = {
...todo,
completed: true
}
})
return {
...state,
entities: newEntities
}
}
case 'todos/completedCleared': {
const newEntities = { ...state.entities }
Object.values(newEntities).forEach(todo => {
if (todo.completed) {
delete newEntities[todo.id]
}
})
return {
...state,
entities: newEntities
}
}
case 'todos/todosLoading': {
return {
...state,
status: 'loading'
}
}
case 'todos/todosLoaded': {
const newEntities = {}
action.payload.forEach(todo => {
newEntities[todo.id] = todo
})
return {
...state,
status: 'idle',
entities: newEntities
}
}
default:
return state
}
}
// omit action creators
const selectTodoEntities = state => state.todos.entities
export const selectTodos = createSelector(selectTodoEntities, entities =>
Object.values(entities)
)
export const selectTodoById = (state, todoId) => {
return selectTodoEntities(state)[todoId]
}
state.entitiesフィールドが配列からオブジェクトに変わったため、データ更新には配列操作ではなくネストされたオブジェクトスプレッド演算子を使用する必要があります。また、オブジェクトを配列のようにループできないため、Object.values(entities)でtodoアイテムの配列を取得してループ処理する必要がある箇所が複数あります。
良い点は、状態検索をカプセル化するためにセレクターを使用しているためUIは変更不要なことです。悪い点は、リデューサーコードが実際にはより長く複雑になったことです。
問題の一部は、このtodoアプリの例が大規模な実践アプリではないことにあります。したがって正規化はこの特定のアプリではあまり有用ではなく、潜在的な利点を見極めるのが困難です。
幸いにも、第8章: Redux Toolkitを使ったモダンReduxでは、正規化された状態を管理するためのリデューサーロジックを大幅に簡略化する方法を紹介します。
現時点で理解すべき重要な点は次の通りです:
-
正規化はReduxアプリで一般的に使用される
-
主な利点はIDで個々のアイテムを検索できることと、状態内にアイテムのコピーが1つだけ存在することを保証できること
Reduxで正規化が有用な理由の詳細については以下を参照してください:
ThunkとPromise
このセクションで最後に取り上げるパターンです。ディスパッチされたアクションに基づいてReduxストアのローディング状態を処理する方法は既に見てきました。コンポーネント内でthunkの結果を確認する必要がある場合はどうすればよいでしょうか?
store.dispatch(action)を呼び出すたびに、dispatchは実際にactionを結果として返します。ミドルウェアはこの動作を変更し、代わりに他の値を返すことができます。
Redux Thunkミドルウェアがdispatchに関数を渡せるようにし、その関数を呼び出して結果を返すことは既に見てきました:
const reduxThunkMiddleware = storeAPI => next => action => {
// If the "action" is actually a function instead...
if (typeof action === 'function') {
// then call the function and pass `dispatch` and `getState` as arguments
// Also, return whatever the thunk function returns
return action(storeAPI.dispatch, storeAPI.getState)
}
// Otherwise, it's a normal action - send it onwards
return next(action)
}
これはPromiseを返すthunk関数を記述し、コンポーネント内でそのPromiseを待機できることを意味します。
<Header>コンポーネントは既にthunkをディスパッチして新しいtodoをサーバーに保存しています。<Header>コンポーネント内にローディング状態を追加し、サーバーの応答を待っている間はテキスト入力を無効にしてもう1つのローディングスピナーを表示しましょう:
const Header = () => {
const [text, setText] = useState('')
const [status, setStatus] = useState('idle')
const dispatch = useDispatch()
const handleChange = e => setText(e.target.value)
const handleKeyDown = async e => {
// If the user pressed the Enter key:
const trimmedText = text.trim()
if (e.which === 13 && trimmedText) {
// Create and dispatch the thunk function itself
setStatus('loading')
// Wait for the promise returned by saveNewTodo
await dispatch(saveNewTodo(trimmedText))
// And clear out the text input
setText('')
setStatus('idle')
}
}
let isLoading = status === 'loading'
let placeholder = isLoading ? '' : 'What needs to be done?'
let loader = isLoading ? <div className="loader" /> : null
return (
<header className="header">
<input
className="new-todo"
placeholder={placeholder}
autoFocus={true}
value={text}
onChange={handleChange}
onKeyDown={handleKeyDown}
disabled={isLoading}
/>
{loader}
</header>
)
}
export default Header
これでtodoを追加すると、ヘッダーにスピナーが表示されます:

学んだこと
ご覧の通り、Reduxアプリではいくつかの追加パターンが広く使用されています。これらのパターンは必須ではなく、最初はより多くのコードを書く必要があるかもしれませんが、ロジックの再利用性の向上、実装詳細のカプセル化、アプリのパフォーマンス改善、データ検索の容易化といった利点があります。
これらのパターンが存在する理由とReduxの使用方法についての詳細は以下を参照してください:
これらのパターンを完全に適用した後のアプリの外観は次のようになります:
- アクションクリエーター関数はアクションオブジェクトとthunkの準備をカプセル化する
- アクションクリエーターは引数を受け取りセットアップロジックを含み、最終的なアクションオブジェクトまたはthunk関数を返す
- メモ化セレクターはReduxアプリのパフォーマンス向上に役立つ
- Reselectはメモ化セレクターを生成する
createSelectorAPIを提供する - メモ化セレクターは同じ入力が与えられた場合、同じ結果参照を返す
- Reselectはメモ化セレクターを生成する
- リクエスト状態はbooleanではなく列挙型(enum)として保存する
'idle'や'loading'などの列挙型を使用すると状態を一貫して追跡できる
- 「Flux Standard Actions」はアクションオブジェクトを整理する一般的な慣例
- アクションはデータに
payload、追加説明にmeta、エラーにerrorを使用する
- アクションはデータに
- 正規化状態によりIDによるアイテム検索が容易になる
- 正規化データは配列ではなくオブジェクトとして保存され、アイテムIDがキーとなる
- Thunkは
dispatchからPromiseを返せる- コンポーネントは非同期thunkの完了を待機してから追加処理を実行できる
次のステップ
これらすべてのコードを「手動で」書くのは時間がかかり困難です。そのため、代わりに公式のRedux Toolkitパッケージを使用してReduxロジックを書くことを推奨します。
Redux Toolkitには、典型的なReduxの使用パターンを少ないコードで実現できるAPIが含まれています。また、誤ってstateを変更するような一般的なミスを防止するのにも役立ちます。
パート8: モダンなReduxでは、これまでに書いたすべてのコードを簡素化するためのRedux Toolkitの使用方法を説明します。