メインコンテンツへスキップ

Reduxの基本、第8部:Redux ToolkitによるモダンなRedux

非公式ベータ版翻訳

このページは PageTurner AI で翻訳されました(ベータ版)。プロジェクト公式の承認はありません。 エラーを見つけましたか? 問題を報告 →

学ぶ内容
  • Redux Toolkitを使ったReduxロジックの簡素化方法
  • Reduxを学び使うための次のステップ

おめでとうございます、チュートリアル最終章に到達しました!完了前にもう1つ重要なトピックを扱います。

これまで学んだ内容を復習したい方は、以下の要約をご覧ください:

情報

Recap: What You've Learned

  • Part 1: Overview:
    • what Redux is, when/why to use it, and the basic pieces of a Redux app
  • Part 2: Concepts and Data Flow:
    • How Redux uses a "one-way data flow" pattern
  • Part 3: State, Actions, and Reducers:
    • Redux state is made of plain JS data
    • Actions are objects that describe "what happened" events in an app
    • Reducers take current state and an action, and calculate a new state
    • Reducers must follow rules like "immutable updates" and "no side effects"
  • Part 4: Store:
    • The createStore API creates a Redux store with a root reducer function
    • Stores can be customized using "enhancers" and "middleware"
    • The Redux DevTools extension lets you see how your state changes over time
  • Part 5: UI and React:
    • Redux is separate from any UI, but frequently used with React
    • React-Redux provides APIs to let React components talk to Redux stores
    • useSelector reads values from Redux state and subscribes to updates
    • useDispatch lets components dispatch actions
    • <Provider> wraps your app and lets components access the store
  • Part 6: Async Logic and Data Fetching:
    • Redux middleware allow writing logic that has side effects
    • Middleware add an extra step to the Redux data flow, enabling async logic
    • Redux "thunk" functions are the standard way to write basic async logic
  • Part 7: Standard Redux Patterns:
    • Action creators encapsulate preparing action objects and thunks
    • Memoized selectors optimize calculating transformed data
    • Request status should be tracked with loading state enum values
    • Normalized state makes it easier to look up items by IDs

見てきたように、Reduxの多くの側面では不変更新、アクションタイプとアクションクリエイター、ステートの正規化など、冗長なコードを書く必要があります。これらのパターンが存在するのには正当な理由がありますが、そのコードを「手動で」書くのは困難です。さらに、Reduxストアのセットアッププロセスには複数のステップが必要で、サンク内での「ローディング」アクションのディスパッチや正規化データの処理など、独自のロジックを考案しなければなりませんでした。最後に、ユーザーはしばしばReduxロジックを書く「正しい方法」が何か確信が持てません。

これがReduxチームがRedux Toolkit:公式のオピニオンが反映された「バッテリー付属」の効率的なRedux開発ツールセットを作成した理由です。

Redux Toolkitには、Reduxアプリ構築に不可欠と考えられるパッケージと関数が含まれています。Redux Toolkitは推奨されるベストプラクティスを組み込み、ほとんどのReduxタスクを簡素化し、一般的なミスを防止し、Reduxアプリケーションの開発を容易にします。

このため、Redux ToolkitはReduxアプリケーションロジックを書く標準的な方法です。このチュートリアルでこれまで書いてきた「手書き」のReduxロジックは実際に動作するコードですが、Reduxロジックを手動で書くべきではありません - このチュートリアルでこれらのアプローチを扱ったのは、Reduxが_どのように_動作するかを理解してもらうためです。しかし、実際のアプリケーションでは、Reduxロジックを書くためにRedux Toolkitを使用すべきです。

Redux Toolkitを使用する場合、これまでに説明したすべての概念(アクション、リデューサー、ストア設定、アクションクリエーター、サンクなど)は依然として存在しますが、Redux Toolkitはそのコードをより簡単に書く方法を提供します

ヒント

Redux ToolkitがカバーするのはReduxロジックのみです。ReactコンポーネントがReduxストアと通信するためには、引き続きReact-Reduxを使用します(useSelectoruseDispatchを含む)。

では、Redux Toolkitを使用して、Todoアプリケーション例ですでに書いたコードをどのように簡略化できるか見てみましょう。主に「スライス」ファイルを書き換えますが、すべてのUIコードはそのまま維持できるはずです。

続ける前に、アプリにRedux Toolkitパッケージを追加してください

npm install @reduxjs/toolkit

ストアの設定

Reduxストアの設定ロジックについては、いくつかのバリエーションを経てきました。現在の設定は次のようになっています:

src/rootReducer.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
src/store.js
import { createStore, applyMiddleware } from 'redux'
import { thunk } from 'redux-thunk'
import { composeWithDevTools } from 'redux-devtools-extension'
import rootReducer from './reducer'

const composedEnhancer = composeWithDevTools(applyMiddleware(thunk))

const store = createStore(rootReducer, composedEnhancer)
export default store

設定プロセスには複数の手順が必要であることに注意してください:

  • スライスリデューサーを結合してルートリデューサーを形成する

  • ストアファイルにルートリデューサーをインポートする

  • サンクミドルウェア、applyMiddlewarecomposeWithDevTools APIをインポートする

  • ミドルウェアとDevToolsを使用してストアエンハンサーを作成する

  • ルートリデューサーでストアを作成する

ここで手順数を削減できれば便利でしょう。

configureStoreの使用

Redux Toolkitにはストア設定プロセスを簡略化するconfigureStore APIがありますconfigureStoreはReduxコアのcreateStore APIをラップし、ストア設定の大部分を自動的に処理します。実際、実質的に1ステップに削減できます:

src/store.js
import { configureStore } from '@reduxjs/toolkit'

import todosReducer from './features/todos/todosSlice'
import filtersReducer from './features/filters/filtersSlice'

const store = configureStore({
reducer: {
// Define a top-level state field named `todos`, handled by `todosReducer`
todos: todosReducer,
filters: filtersReducer
}
})

export default store

このconfigureStoreの1回の呼び出しがすべての作業を行いました:

  • todosReducerfiltersReducerをルートリデューサー関数に結合し、{todos, filters}のようなルートステートを処理する

  • そのルートリデューサーを使用してReduxストアを作成した

  • thunkミドルウェアを自動的に追加した

  • 誤ってステートをミューテートするような一般的なミスを検出するための追加ミドルウェアを自動的に設定した

  • Redux DevTools Extensionの接続を自動的にセットアップした

Todoアプリケーション例を開いて使用することで、これが機能することを確認できます。既存の機能コードはすべて正常に動作します!アクションをディスパッチし、サンクをディスパッチし、UIでステートを読み取り、DevToolsでアクション履歴を確認しているため、これらのすべての部分が正しく動作しているに違いありません。変更したのはストア設定コードだけです。

誤ってステートの一部をミューテートするとどうなるか見てみましょう。「todos loading」リデューサーを、不変なコピーを作成する代わりにステートフィールドを直接変更するように変更した場合:

src/features/todos/todosSlice
export default function todosReducer(state = initialState, action) {
switch (action.type) {
// omit other cases
case 'todos/todosLoading': {
// ❌ WARNING: example only - don't do this in a normal reducer!
state.status = 'loading'
return state
}
default:
return state
}
}

おっと、アプリ全体がクラッシュしました!何が起きたのでしょうか?

不変性チェックミドルウェアエラー

このエラーメッセージは良いことです - アプリのバグを検出しました! configureStoreは特に、ステートの誤った変更を検出すると(開発モードのみ)自動的にエラーをスローする追加ミドルウェアを組み込みました。これにより、コーディング中のミスを捕捉できます。

パッケージの整理

Redux Toolkitには既にreduxredux-thunkreselectなど、使用しているいくつかのパッケージが含まれており、それらのAPIを再エクスポートしています。そのため、プロジェクトを少し整理できます。

まず、createSelectorのインポートを'reselect'から'@reduxjs/toolkit'に切り替えられます。次に、package.jsonにリストされている個別のパッケージを削除できます:

npm uninstall redux redux-thunk reselect

明確にしておくと、これらのパッケージは依然として使用されており、インストールする必要があります。ただし、Redux Toolkitが依存しているため、@reduxjs/toolkitをインストールすると自動的にインストールされるので、package.jsonファイルに個別に記載する必要はありません。

スライスの記述

アプリに新機能を追加するにつれて、スライスファイルはより大きく複雑になってきました。特に、todosReducerは不変更新のためのネストされたオブジェクトスプレッドが多く、可読性が低下しています。また、複数のアクションクリエーター関数を記述しました。

Redux ToolkitのcreateSlice APIを使用すると、Reduxのリデューサーロジックとアクションを簡素化できますcreateSliceは次の重要な機能を提供します:

  • switch/case文を書く代わりに、オブジェクト内に関数としてケースリデューサーを記述可能

  • より短い不変更新ロジックを記述可能

  • 提供したリデューサー関数に基づいて、すべてのアクションクリエーターを自動生成

createSliceの使用

createSliceは3つの主要なオプションを持つオブジェクトを受け取ります:

  • name:生成されるアクションタイプのプレフィックスとして使用される文字列

  • initialState:リデューサーの初期状態

  • reducers:キーが文字列、値が特定のアクションを処理する「ケースリデューサー」関数であるオブジェクト

まずは独立した小さな例を見てみましょう。

createSlice example
import { createSlice } from '@reduxjs/toolkit'

const initialState = {
entities: [],
status: null
}

const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
todoAdded(state, action) {
// ✅ This "mutating" code is okay inside of createSlice!
state.entities.push(action.payload)
},
todoToggled(state, action) {
const todo = state.entities.find(todo => todo.id === action.payload)
todo.completed = !todo.completed
},
todosLoading(state, action) {
return {
...state,
status: 'loading'
}
}
}
})

export const { todoAdded, todoToggled, todosLoading } = todosSlice.actions

export default todosSlice.reducer

この例で注目すべき点がいくつかあります:

  • reducersオブジェクト内に可読性の高い名前でケースリデューサー関数を記述

  • createSliceは提供したケースリデューサー関数に対応するアクションクリエーターを自動生成

  • createSliceはデフォルトケースで既存の状態を自動的に返す

  • createSliceは状態を安全に「変更」できるようにする!

  • 従来通り不変コピーを作成することも可能

生成されたアクションクリエーターはslice.actions.todoAddedとして利用可能で、従来と同様に個別に分割代入してエクスポートします。完全なリデューサー関数はslice.reducerとして利用可能で、これも従来通りexport default slice.reducerします。

自動生成されたアクションオブジェクトはどのようなものでしょうか?実際に呼び出してログに表示してみましょう:

console.log(todoToggled(42))
// {type: 'todos/todoToggled', payload: 42}

createSliceはスライスのnameフィールドとリデューサー関数名todoToggledを組み合わせてアクションタイプ文字列を生成しました。デフォルトでは、アクションクリエーターは1つの引数を受け取り、それをaction.payloadとしてアクションオブジェクトに配置します。

生成されたリデューサー関数内部では、createSliceはディスパッチされたアクションのaction.typeが生成した名前のいずれかと一致するか確認します。一致する場合、そのケースリデューサー関数を実行します。これはswitch/case文を使用した従来のパターンと全く同じですが、createSliceが自動的に処理します。

「変更」の側面についても詳しく説明する価値があります。

Immerを使った不変更新

先ほど、「ミューテーション」(既存のオブジェクト/配列値の変更)と「イミュータビリティ」(値を変更不可能なものとして扱うこと)について説明しました。

警告

Reduxでは、リデューサーが元の/現在のstate値を変更することは_絶対に_許可されていません!

// ❌ Illegal - by default, this will mutate the state!
state.value = 123

では、元の値を変更できない場合、更新された状態をどのように返せばよいのでしょうか?

ヒント

リデューサーは元の値のコピーを作成し、そのコピーを変更する必要があります。

// This is safe, because we made a copy
return {
...state,
value: 123
}

このチュートリアルで見てきたように、JavaScriptの配列/オブジェクトスプレッド演算子や元の値のコピーを返す関数を使って、手動で不変更新を書くことは可能です。しかし、手動での不変更新ロジックの記述は難しく、リデューサー内での誤った状態変更はReduxユーザーが最も頻繁に犯すミスです。

これが、Redux ToolkitのcreateSlice関数が不変更新を簡単に書けるようにしている理由です!

createSliceは内部でImmerというライブラリを使用しています。ImmerはProxyと呼ばれる特別なJSツールを使用して提供されたデータをラップし、「変更」するコードを書けるようにします。しかし、Immerはあなたが行おうとしたすべての変更を追跡し、その変更リストを使用して安全に不変更新された値を返します。まるであなたが手動で不変更新ロジックを書いたかのようにです。

したがって、このようなコードの代わりに:

function handwrittenReducer(state, action) {
return {
...state,
first: {
...state.first,
second: {
...state.first.second,
[action.someId]: {
...state.first.second[action.someId],
fourth: action.someValue
}
}
}
}
}

このようなコードを書くことができます:

function reducerWithImmer(state, action) {
state.first.second[action.someId].fourth = action.someValue
}

これはずっと読みやすいです!

しかし、非常に重要なことを覚えておいてください:

警告

Redux ToolkitのcreateSlicecreateReducerは内部でImmerを使用しているため、ここでのみ「変更」ロジックを書けます!Immerなしでリデューサーに変更ロジックを書くと、状態が変更されてバグが発生します!

Immerは依然として、必要に応じて手動で不変更新を書き、新しい値を自分で返すことも可能です。両方を混在させることもできます。例えば、配列からアイテムを削除する場合はarray.filter()を使う方が簡単なことが多いため、それを呼び出して結果をstateに「変更」として割り当てられます:

// can mix "mutating" and "immutable" code inside of Immer:
state.todos = state.todos.filter(todo => todo.id !== action.payload)

Todosリデューサーの変換

まずtodosスライスファイルをcreateSliceを使用するように変換しましょう。プロセスの仕組みを示すために、switch文からいくつかのケースを選んで説明します。

src/features/todos/todosSlice.js
import { createSlice } from '@reduxjs/toolkit'

const initialState = {
status: 'idle',
entities: {}
}

const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
todoAdded(state, action) {
const todo = action.payload
state.entities[todo.id] = todo
},
todoToggled(state, action) {
const todoId = action.payload
const todo = state.entities[todoId]
todo.completed = !todo.completed
}
}
})

export const { todoAdded, todoToggled } = todosSlice.actions

export default todosSlice.reducer

サンプルアプリのtodosリデューサーは、親オブジェクトにネストされた正規化状態を使用しているため、先ほど見た小規模なcreateSliceの例とは少し異なります。以前todoのトグル操作に多くのネストされたスプレッド演算子が必要だったことを覚えていますか?今では同じコードが大幅に短く読みやすくなりました。

このリデューサーにさらにいくつかのケースを追加しましょう。

src/features/todos/todosSlice.js
const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
todoAdded(state, action) {
const todo = action.payload
state.entities[todo.id] = todo
},
todoToggled(state, action) {
const todoId = action.payload
const todo = state.entities[todoId]
todo.completed = !todo.completed
},
todoColorSelected: {
reducer(state, action) {
const { color, todoId } = action.payload
state.entities[todoId].color = color
},
prepare(todoId, color) {
return {
payload: { todoId, color }
}
}
},
todoDeleted(state, action) {
delete state.entities[action.payload]
}
}
})

export const { todoAdded, todoToggled, todoColorSelected, todoDeleted } =
todosSlice.actions

export default todosSlice.reducer

todoAddedtodoToggledのアクションクリエーターは、todoオブジェクト全体やtodo IDといった単一のパラメータのみを取ります。しかし、複数のパラメータを渡す必要がある場合や、一意のID生成といった「準備」ロジックを実装する場合はどうでしょうか?

createSliceは、リデューサーに「prepareコールバック」を追加することでこれらの状況を処理できます。reducerprepareという関数を持つオブジェクトを渡せます。生成されたアクションクリエーターを呼び出すと、渡されたパラメータを使ってprepare関数が呼び出されます。この関数はFlux Standard Action規約に沿って、payloadフィールド(オプションでmetaerrorフィールド)を持つオブジェクトを作成して返す必要があります。

ここではprepareコールバックを使用して、todoColorSelectedアクションクリエーターが別々のtodoIdcolor引数を受け取り、それらをaction.payload内のオブジェクトとしてまとめています。

一方、todoDeletedリデューサーでは、JavaScriptのdelete演算子を使用して正規化状態からアイテムを削除できます。

これらの同じパターンを使用して、todosSlice.jsfiltersSlice.jsの残りのリデューサーを書き換えられます。

すべてのスライスを変換した後のコードは次のようになります:

Thunkの記述

「loading」「request succeeded」「request failed」アクションをディスパッチするThunkの書き方を既に見てきました。これらのケースを処理するためには、アクションクリエーター、アクションタイプ、リデューサーをすべて書く必要がありました。

このパターンは非常に一般的であるため、Redux ToolkitにはこれらのThunkを生成するcreateAsyncThunk APIがあります。また、異なるリクエストステータスアクションのアクションタイプとアクションクリエーターも生成し、結果のPromiseに基づいてそれらのアクションを自動的にディスパッチします。

ヒント

Redux Toolkitには新しいRTK Queryデータ取得APIがあります。RTK QueryはReduxアプリ向けに特別に構築されたデータ取得・キャッシュソリューションであり、データ取得管理のために_thunk_や_reducer_を書く必要を完全になくすことができます。ぜひお試しいただき、ご自身のアプリでデータ取得コードを簡素化できるか確認してください!

Reduxチュートリアルは近日中に更新され、RTK Queryの使用方法に関するセクションが追加される予定です。それまでは、Redux ToolkitドキュメントのRTK Queryセクションをご参照ください。

createAsyncThunkの使用

createAsyncThunkを使用してthunkを生成し、fetchTodos thunkを置き換えましょう。

createAsyncThunkは2つの引数を受け取ります:

  • 生成されるアクションタイプのプレフィックスとして使用される文字列

  • Promiseを返す「ペイロードクリエーター」コールバック関数。async関数は自動的にPromiseを返すため、async/await構文で書かれることが多いです。

src/features/todos/todosSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'

// omit imports and state

export const fetchTodos = createAsyncThunk('todos/fetchTodos', async () => {
const response = await client.get('/fakeApi/todos')
return response.todos
})

const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
// omit reducer cases
},
extraReducers: builder => {
builder
.addCase(fetchTodos.pending, (state, action) => {
state.status = 'loading'
})
.addCase(fetchTodos.fulfilled, (state, action) => {
const newEntities = {}
action.payload.forEach(todo => {
newEntities[todo.id] = todo
})
state.entities = newEntities
state.status = 'idle'
})
}
})

// omit exports

'todos/fetchTodos'を文字列プレフィックスとして渡し、APIを呼び出して取得データを含むPromiseを返す「ペイロードクリエーター」関数を渡します。内部でcreateAsyncThunkは3つのアクションクリエーターとアクションタイプ、さらに呼び出時に自動的にこれらのアクションをディスパッチするthunk関数を生成します。この場合のアクションクリエーターとタイプは:

  • fetchTodos.pending: todos/fetchTodos/pending

  • fetchTodos.fulfilled: todos/fetchTodos/fulfilled

  • fetchTodos.rejected: todos/fetchTodos/rejected

ただし、これらのアクションクリエーターとタイプはcreateSlice呼び出しの_外部_で定義されています。これらをcreateSlice.reducersフィールド内で処理することはできません。なぜなら、そこでも新たなアクションタイプが生成されるからです。createSlice呼び出しが他の場所で定義されたアクションタイプをリッスンする方法が必要です。

createSliceextraReducersオプションも受け付け、同じスライスリデューサーが他のアクションタイプをリッスンできるようにします。このフィールドはbuilderパラメータを持つコールバック関数で、builder.addCase(actionCreator, caseReducer)を呼び出して他のアクションをリッスンできます。

ここではbuilder.addCase(fetchTodos.pending, caseReducer)を呼び出しています。このアクションがディスパッチされると、スイッチステートメントで実装した時と同様にstate.status = 'loading'を設定するリデューサーが実行されます。fetchTodos.fulfilledについても同様に処理し、APIから受け取ったデータを扱います。

別の例としてsaveNewTodoを変換しましょう。このthunkは新しいtodoオブジェクトのtextをパラメータとして受け取り、サーバーに保存します。どう扱えばよいでしょうか?

src/features/todos/todosSlice.js
// omit imports

export const fetchTodos = createAsyncThunk('todos/fetchTodos', async () => {
const response = await client.get('/fakeApi/todos')
return response.todos
})

export const saveNewTodo = createAsyncThunk('todos/saveNewTodo', async text => {
const initialTodo = { text }
const response = await client.post('/fakeApi/todos', { todo: initialTodo })
return response.todo
})

const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
// omit case reducers
},
extraReducers: builder => {
builder
.addCase(fetchTodos.pending, (state, action) => {
state.status = 'loading'
})
.addCase(fetchTodos.fulfilled, (state, action) => {
const newEntities = {}
action.payload.forEach(todo => {
newEntities[todo.id] = todo
})
state.entities = newEntities
state.status = 'idle'
})
.addCase(saveNewTodo.fulfilled, (state, action) => {
const todo = action.payload
state.entities[todo.id] = todo
})
}
})

// omit exports and selectors

saveNewTodoのプロセスはfetchTodosと同様です。createAsyncThunkを呼び出し、アクションプレフィックスとペイロードクリエーターを渡します。ペイロードクリエーター内で非同期API呼び出しを行い、結果値を返します。

この場合、dispatch(saveNewTodo(text))を呼び出すと、text値がペイロードクリエーターの第一引数として渡されます。

ここではcreateAsyncThunkの詳細には触れませんが、参考までにいくつかの注意点を挙げます:

  • thunkをディスパッチする際に渡せる引数は一つだけです。複数の値を渡す必要がある場合は、単一オブジェクトにまとめてください

  • ペイロードクリエーターは第二引数として{getState, dispatch}やその他の有用な値を含むオブジェクトを受け取ります

  • thunkはペイロードクリエーター実行前にpendingアクションをディスパッチし、返したPromiseの成功・失敗に応じてfulfilledまたはrejectedをディスパッチします

状態の正規化(Normalizing State)

以前、アイテムIDをキーとしたオブジェクトにアイテムを保持することで状態を「正規化」する方法を見てきました。これにより、配列全体をループせずにIDでアイテムを検索できます。しかし、正規化された状態を手動で更新するロジックは冗長で退屈でした。Immerを使った「ミューテート」形式の更新コードはこれを簡略化しますが、依然として繰り返しが多くなりがちです。アプリで多くの異なるタイプのアイテムを読み込む場合、毎回同じリデューサロジックを繰り返す必要があります。

Redux ToolkitにはcreateEntityAdapter APIが含まれており、正規化された状態に対する典型的なデータ更新操作のための事前構築済みリデューサを提供します。これにはスライスからのアイテムの追加、更新、削除が含まれます。createEntityAdapterはまた、ストアから値を読み取るためのメモ化されたセレクタも生成します

createEntityAdapterの使用

正規化されたエンティティのリデューサロジックをcreateEntityAdapterで置き換えましょう。

createEntityAdapterを呼び出すと、以下の事前作成済みリデューサ関数を含む「アダプタ」オブジェクトが得られます:

  • addOne / addMany: 新しいアイテムを状態に追加

  • upsertOne / upsertMany: 新しいアイテムを追加または既存のアイテムを更新

  • updateOne / updateMany: 部分的な値を指定して既存アイテムを更新

  • removeOne / removeMany: IDに基づいてアイテムを削除

  • setAll: 既存のアイテムをすべて置換

これらの関数はケースリデューサとして、またはcreateSlice内の「ミューテーションヘルパー」として使用できます。

アダプタには以下も含まれます:

  • getInitialState: { ids: [], entities: {} }形式のオブジェクトを返し、全アイテムIDの配列と共に正規化された状態を保存

  • getSelectors: 標準的なセレクタ関数セットを生成

Todoスライスでこれらをどのように使用するか見てみましょう:

src/features/todos/todosSlice.js
import {
createSlice,
createAsyncThunk,
createEntityAdapter
} from '@reduxjs/toolkit'
// omit some imports

const todosAdapter = createEntityAdapter()

const initialState = todosAdapter.getInitialState({
status: 'idle'
})

// omit thunks

const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
// omit some reducers
// Use an adapter reducer function to remove a todo by ID
todoDeleted: todosAdapter.removeOne,
completedTodosCleared(state, action) {
const completedIds = Object.values(state.entities)
.filter(todo => todo.completed)
.map(todo => todo.id)
// Use an adapter function as a "mutating" update helper
todosAdapter.removeMany(state, completedIds)
}
},
extraReducers: builder => {
builder
.addCase(fetchTodos.pending, (state, action) => {
state.status = 'loading'
})
.addCase(fetchTodos.fulfilled, (state, action) => {
todosAdapter.setAll(state, action.payload)
state.status = 'idle'
})
// Use another adapter function as a reducer to add a todo
.addCase(saveNewTodo.fulfilled, todosAdapter.addOne)
}
})

// omit selectors

異なるアダプタリデューサ関数は、関数に応じてaction.payload内の異なる値を受け取ります。「追加」や「アップサート」関数は単一アイテムまたはアイテムの配列を受け取り、「削除」関数は単一IDまたはIDの配列を受け取ります。

getInitialStateでは、追加の状態フィールドを含めることができます。このケースではstatusフィールドを渡し、以前と同様の最終的なTodoスライス状態{ids, entities, status}を得ています。

Todoセレクタ関数の一部も置き換えられます。getSelectorsアダプタ関数はselectAll(全アイテムの配列を返す)やselectById(単一アイテムを返す)などのセレクタを生成します。ただし、getSelectorsはデータがRedux状態ツリー全体のどこにあるか知らないため、状態ツリー全体からこのスライスを返す小さなセレクタを渡す必要があります。これらを使用するように切り替えましょう。これが最後の主要なコード変更となるため、Redux Toolkitを使用した最終的なコードの全体像を見るために、今回のTodoスライスファイル全体を含めます:

src/features/todos/todosSlice.js
import {
createSlice,
createSelector,
createAsyncThunk,
createEntityAdapter
} from '@reduxjs/toolkit'
import { client } from '../../api/client'
import { StatusFilters } from '../filters/filtersSlice'

const todosAdapter = createEntityAdapter()

const initialState = todosAdapter.getInitialState({
status: 'idle'
})

// Thunk functions
export const fetchTodos = createAsyncThunk('todos/fetchTodos', async () => {
const response = await client.get('/fakeApi/todos')
return response.todos
})

export const saveNewTodo = createAsyncThunk('todos/saveNewTodo', async text => {
const initialTodo = { text }
const response = await client.post('/fakeApi/todos', { todo: initialTodo })
return response.todo
})

const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
todoToggled(state, action) {
const todoId = action.payload
const todo = state.entities[todoId]
todo.completed = !todo.completed
},
todoColorSelected: {
reducer(state, action) {
const { color, todoId } = action.payload
state.entities[todoId].color = color
},
prepare(todoId, color) {
return {
payload: { todoId, color }
}
}
},
todoDeleted: todosAdapter.removeOne,
allTodosCompleted(state, action) {
Object.values(state.entities).forEach(todo => {
todo.completed = true
})
},
completedTodosCleared(state, action) {
const completedIds = Object.values(state.entities)
.filter(todo => todo.completed)
.map(todo => todo.id)
todosAdapter.removeMany(state, completedIds)
}
},
extraReducers: builder => {
builder
.addCase(fetchTodos.pending, (state, action) => {
state.status = 'loading'
})
.addCase(fetchTodos.fulfilled, (state, action) => {
todosAdapter.setAll(state, action.payload)
state.status = 'idle'
})
.addCase(saveNewTodo.fulfilled, todosAdapter.addOne)
}
})

export const {
allTodosCompleted,
completedTodosCleared,
todoAdded,
todoColorSelected,
todoDeleted,
todoToggled
} = todosSlice.actions

export default todosSlice.reducer

export const { selectAll: selectTodos, selectById: selectTodoById } =
todosAdapter.getSelectors(state => state.todos)

export const selectTodoIds = createSelector(
// First, pass one or more "input selector" functions:
selectTodos,
// Then, an "output selector" that receives all the input results as arguments
// and returns a final result value
todos => todos.map(todo => todo.id)
)

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

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)
)

todosAdapter.getSelectorsを呼び出し、状態のこのスライスを返すstate => state.todosセレクタを渡します。アダプタはそこから、通常通りRedux状態ツリー全体を受け取るselectAllセレクタを生成し、state.todos.entitiesstate.todos.idsをループしてTodoオブジェクトの完全な配列を提供します。selectAllでは何を選択しているか明確でないため、分割代入構文を使用して関数名をselectTodosに変更できます。同様に、selectByIdselectTodoByIdにリネームできます。

他のセレクタが入力としてselectTodosを使い続けていることに注目してください。これは、state.todos全体を配列として保持する場合でも、ネストした配列として保持する場合でも、正規化されたオブジェクトとして保存して配列に変換する場合でも、常にtodoオブジェクトの配列を返しているためです。データの保存方法をすべて変更しても、セレクタを使用することでコードの他の部分を同じに保つことができ、メモ化されたセレクタは不必要な再レンダーを避けることでUIのパフォーマンス向上に役立ちました。

学んだこと

おめでとうございます!「Redux Fundamentals」チュートリアルを完了しました!

これで、Reduxが何であるか、どのように動作するか、そして正しい使い方をしっかり理解できたはずです:

  • グローバルなアプリケーションの状態管理

  • アプリの状態をプレーンなJSデータとして保持すること

  • アプリ内で「何が起こったか」を記述するアクションオブジェクトの記述

  • 現在の状態とアクションを参照し、それに応じて不変的に新しい状態を作成するリデューサ関数の使用

  • useSelectorを使用してReactコンポーネント内でReduxの状態を読み取ること

  • useDispatchを使用してReactコンポーネントからアクションをディスパッチすること

さらに、Redux ToolkitがReduxロジックの記述をどのように簡素化するか、そしてRedux Toolkitが実際のReduxアプリケーションを書くための標準的なアプローチである理由を学びました。まず手動でReduxコードを書く方法を見たことで、createSliceのようなRedux ToolkitのAPIが何をしているのかが明確になり、そのコードを自分で書く必要がなくなったはずです。

情報

Redux Toolkitの使用ガイドやAPIリファレンスを含む詳細については、以下を参照してください:

Redux Toolkitを使用するように変換されたすべてのコードを含む、完成したtodoアプリケーションを最後に見てみましょう:

そして、このセクションで学んだ重要なポイントを最終的にまとめます:

まとめ
  • Redux Toolkit(RTK)はReduxロジックを記述する標準的な方法です
    • RTKには、ほとんどのReduxコードを簡素化するAPIが含まれています
    • RTKはReduxコアをラップし、他の便利なパッケージも含みます
  • configureStoreはデフォルト設定でReduxストアをセットアップします
    • スライスリデューサを自動的に結合してルートリデューサを作成します
    • Redux DevTools拡張とデバッグ用ミドルウェアを自動的にセットアップします
  • createSliceはReduxアクションとリデューサの記述を簡素化します
    • スライス/リデューサ名に基づいてアクションクリエーターを自動生成します
    • リデューサはImmerを使用してcreateSlice内で状態を「変更」できます
  • createAsyncThunkは非同期呼び出しのためのサンクを生成します
    • サンクとpending/fulfilled/rejectedアクションクリエーターを自動生成します
    • サンクをディスパッチするとペイロードクリエーターが実行され、アクションがディスパッチされます
    • サンクアクションはcreateSlice.extraReducersで処理できます
  • createEntityAdapterは正規化された状態のためのリデューサとセレクタを提供します
    • アイテムの追加/更新/削除のような一般的なタスクのためのリデューサ関数を含みます
    • selectAllselectByIdのためのメモ化されたセレクタを生成します

Reduxの学習と使用の次のステップ

このチュートリアルを完了したので、Reduxについてさらに学ぶために次に試すべきことをいくつか提案します。

この「Fundamentals」チュートリアルは、Reduxの低レベルな側面、つまり手動でのアクションタイプと不変更新の記述、Reduxストアとミドルウェアの動作方法、アクションクリエーターや正規化された状態のようなパターンを使用する理由に焦点を当てました。さらに、todoのサンプルアプリはかなり小さく、本格的なアプリを構築する現実的な例として意図されていません。

しかし、「Redux Essentials」チュートリアルでは、特に**「実世界」レベルのアプリケーション構築方法**を教えています。このチュートリアルではRedux Toolkitを使った「正しいReduxの使い方」に焦点を当て、大規模アプリで見られる現実的なパターンについて解説しています。「Fundamentals」チュートリアルと同様にリデューサーが不変更新が必要な理由などのトピックも扱いますが、実際に動作するアプリケーション構築に重点を置いています。次のステップとして「Redux Essentials」チュートリアルの読破を強くお勧めします。

同時に、このチュートリアルで学んだ概念は、ReactとReduxを使って独自のアプリケーションを構築し始めるのに十分なはずです。概念を定着させ、実際にどのように機能するかを確認するために、自身でプロジェクトに取り組む絶好の機会です。どのようなプロジェクトを構築すべきかわからない場合は、アプリケーションのアイデアリストを参考にしてください。

Reduxの使用セクションには、リデューサーの構造化方法など多くの重要な概念に関する情報があります。また、スタイルガイドには推奨パターンとベストプラクティスに関する重要な情報が記載されています。

Reduxが_なぜ_存在するのか、どのような問題を解決しようとしているのか、どのように使用されることを意図しているのかについてさらに知りたい場合は、ReduxメンテナーのMark EriksonによるThe Tao of Redux, Part 1: Implementation and IntentThe Tao of Redux, Part 2: Practice and Philosophyの記事を参照してください。

Reduxに関する質問のサポートが必要な場合は、DiscordのReactifluxサーバー内の#reduxチャンネルに参加してください。

このチュートリアルをお読みいただきありがとうございます。Reduxでのアプリケーション構築をお楽しみください!