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

Reduxの基本、パート8:Redux Toolkitを使ったモダンなRedux

学習内容
  • Redux Toolkitを使用してReduxロジックを簡素化する方法
  • Reduxを学習および使用するための次の手順

おめでとうございます。このチュートリアルの最後のセクションに到達しました。終了する前に、もう1つトピックがあります。

これまでに学習した内容を思い出したい場合は、この概要をご覧ください。

情報

要約:学習内容

  • パート1:概要:
    • Reduxとは何か、いつ/なぜ使用するのか、Reduxアプリの基本的な構成要素
  • パート2:概念とデータフロー:
    • Reduxが「単方向データフロー」パターンをどのように使用するか
  • パート3:ステート、アクション、レデューサー:
    • ReduxステートはプレーンなJSデータで構成されている
    • アクションは、アプリで「何が起こったか」を表すオブジェクト
    • レデューサーは現在のステートとアクションを受け取り、新しいステートを計算する
    • レデューサーは、「不変の更新」や「副作用なし」などのルールに従う必要がある
  • パート4:ストア:
    • createStore APIは、ルートレデューサー関数を使用してReduxストアを作成する
    • ストアは、「エンハンサー」と「ミドルウェア」を使用してカスタマイズできる
    • Redux DevTools拡張機能を使用すると、ステートが時間とともにどのように変化するかを確認できる
  • パート5:UIとReact:
    • ReduxはUIから独立しているが、Reactで頻繁に使用される
    • React-Reduxは、ReactコンポーネントがReduxストアと通信するためのAPIを提供する
    • useSelectorはReduxステートから値を読み取り、更新を購読する
    • useDispatchを使用すると、コンポーネントはアクションをディスパッチできる
    • <Provider>はアプリをラップし、コンポーネントがストアにアクセスできるようにする
  • パート6:非同期ロジックとデータフェッチ:
    • Reduxミドルウェアを使用すると、副作用のあるロジックを記述できる
    • ミドルウェアはReduxデータフローに余分な手順を追加し、非同期ロジックを有効にする
    • Redux「サンク」関数は、基本的な非同期ロジックを記述するための標準的な方法
  • パート7:標準的なReduxパターン:
    • アクションクリエーターは、アクションオブジェクトとサンクの準備をカプセル化する
    • メモ化されたセレクターは、変換されたデータの計算を最適化する
    • リクエストステータスは、読み込み状態の列挙値で追跡する必要がある
    • 正規化された状態により、IDによるアイテムの検索が容易になる

見てきたように、Reduxの多くの側面には、不変の更新、アクションタイプとアクションクリエーター、状態の正規化など、冗長なコードを記述することが含まれます。 これらのパターンが存在するのには正当な理由がありますが、そのコードを「手書き」で記述することは難しい場合があります。 さらに、Reduxストアをセットアップするプロセスにはいくつかの手順が必要であり、サンクで「読み込み中」アクションをディスパッチしたり、正規化されたデータを処理したりするための独自のロジックを考え出す必要がありました。 最後に、多くの場合、ユーザーはReduxロジックを記述する「正しい方法」がわかりません。

そのため、ReduxチームはRedux Toolkitを作成しました。これは、効率的なRedux開発のための公式の、意見のある、「バッテリー 포함」ツールセットです。

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

このため、Redux ToolkitはReduxアプリケーションロジックを記述するための標準的な方法です。 このチュートリアルでこれまでに記述した「手書き」のReduxロジックは実際に機能するコードですが、Reduxロジックを手書きで記述しないでください - このチュートリアルでは、Reduxの動作を理解するために、これらのアプローチについて説明しました。 ただし、実際のアプリケーションでは、Redux Toolkitを使用してReduxロジックを記述する必要があります。

Redux Toolkitを使用する場合、これまでに説明したすべての概念(アクション、レデューサー、ストアのセットアップ、アクションクリエーター、サンクなど)は引き続き存在しますが、Redux Toolkitは、そのコードを記述するより簡単な方法を提供します

ヒント

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

それでは、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 thunkMiddleware from 'redux-thunk'
import { composeWithDevTools } from 'redux-devtools-extension'
import rootReducer from './reducer'

const composedEnhancer = composeWithDevTools(applyMiddleware(thunkMiddleware))

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

セットアッププロセスにはいくつかの手順が必要です。 次のことを行う必要があります

  • スライスレデューサーを組み合わせてルートレデューサーを形成する
  • ルートレデューサーをストアファイルにインポートする
  • サンクミドルウェア、applyMiddleware、およびcomposeWithDevTools APIをインポートする
  • ミドルウェアと開発ツールを使用してストアエンハンサーを作成する
  • ルートレデューサーを使用してストアを作成する

ここで手順の数を減らすことができればいいのですが。

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ストアを作成しました
  • サンクミドルウェアが自動的に追加されました
  • 状態を誤って変更するなどの一般的な間違いをチェックするためのミドルウェアが自動的に追加されました
  • Redux DevTools拡張機能の接続が自動的に設定されました

サンプルの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
}
}

ああ。 アプリ全体がクラッシュしました。 どうしたの?

Immutability check middleware error

このエラーメッセージは*良い*ことです-アプリのバグをキャッチしました。 configureStoreは、状態の偶発的な変更が発生するたびに(開発モードのみ)自動的にエラーをスローする特別なミドルウェアを追加しました。 これは、コードの記述中に発生する可能性のある間違いをキャッチするのに役立ちます。

パッケージのクリーンアップ

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

まず、createSelectorインポートを'reselect'ではなく'@reduxjs/toolkit'からに変更できます。 次に、package.jsonにリストされている個別の paket を削除できます

npm uninstall redux redux-thunk reselect

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

スライスの記述

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

Redux Toolkitには、Reduxのreducerロジックとアクションを簡素化するのに役立つ `createSlice` APIがあります。 `createSlice` は、私たちのためにいくつかの重要なことを行います。

  • `switch/case` 文を記述する代わりに、ケースreducerをオブジェクト内の関数として記述できます。
  • reducerは、より短いイミュータブルな更新ロジックを記述できます。
  • すべてのアクションクリエーターは、提供したreducer関数に基づいて自動的に生成されます。

`createSlice` の使用

`createSlice` は、3つの主要なオプションフィールドを持つオブジェクトを受け取ります。

  • `name`:生成されるアクションタイプのプレフィックスとして使用される文字列
  • `initialState`:reducerの初期状態
  • `reducers`:キーが文字列で、値が特定のアクションを処理する「ケースreducer」関数であるオブジェクト

まず、小さなスタンドアロンの例を見てみましょう。

createSlice の例
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

この例では、いくつかの点に注目してください。

  • ケースreducer関数を `reducers` オブジェクト内に記述し、読みやすい名前を付けます。
  • `createSlice` は、提供する各ケースreducer関数に対応するアクションクリエーターを自動的に生成します
  • `createSlice` は、デフォルトケースで既存の状態を自動的に返します。
  • `createSlice` を使用すると、状態を安全に「変更」できます!
  • ただし、必要に応じて、以前のようにイミュータブルなコピーを作成することもできます。

生成されたアクションクリエーターは、`slice.actions.todoAdded` として利用でき、通常は、以前に記述したアクションクリエーターと同様に、個別に分割代入してエクスポートします。完全なreducer関数は `slice.reducer` として利用でき、通常は `export default slice.reducer` と記述します。これも以前と同じです。

では、これらの自動生成されたアクションオブジェクトはどのようなものでしょうか?それらの1つを呼び出して、アクションをログに記録して確認してみましょう。

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

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

生成されたreducer関数内では、`createSlice` は、ディスパッチされたアクションの `action.type` が、生成された名前のいずれかと一致するかどうかを確認します。一致する場合、そのケースreducer関数を実行します。これは、`switch/case` 文を使用して自分で記述したパターンとまったく同じですが、`createSlice` は自動的に実行します。

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

Immerによるイミュータブルな更新

以前、「変更」(既存のオブジェクト/配列値の変更)と「イミュータビリティ」(値を変更できないものとして扱うこと)について説明しました。

危険

Reduxでは、reducerは *決して* 元の/現在の状態値を変更することはできません!

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

では、オリジナルを変更できない場合、どのように更新された状態を返すのでしょうか?

ヒント

reducerは、元の値の *コピー* のみを作成でき、コピーを変更できます。

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

このチュートリアル全体で見てきたように、JavaScriptの配列/オブジェクトスプレッド演算子や、元の値のコピーを返す他の関数を使用して、手動でイミュータブルな更新を記述できます。ただし、イミュータブルな更新ロジックを手動で記述するのは *難しい* ことであり、reducerで誤って状態を変更してしまうことは、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の `createSlice` と `createReducer` は内部でImmerを使用しているため、これらの内部で *のみ* 「変更」ロジックを記述できます。 Immerなしでreducerに変更ロジックを記述すると、状態が *変更* され、バグが発生します!

Immerでは、必要に応じて、手動でイミュータブルな更新を記述して、新しい値を自分で返すこともできます。組み合わせることもできます。たとえば、配列からアイテムを削除することは、多くの場合 `array.filter()` を使用した方が簡単なので、それを呼び出してから結果を `state` に割り当てて「変更」できます。

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

Todos Reducerの変換

`createSlice` を使用するために、todosスライスファイルの変換を始めましょう。まず、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 reducerは、親オブジェクトにネストされた正規化された状態を使用しているため、ここでのコードは、先ほど見たミニチュアの `createSlice` 例とは少し異なります。 以前にtodoを切り替えるために、多数のネストされたスプレッド演算子を記述する必要があったことを覚えていますか? अब वही कोड *बहुत* छोटा और पढ़ने में आसान है।

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

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

`todoAdded` と `todoToggled` のアクションクリエーターは、todoオブジェクト全体やtodo IDなど、単一のパラメーターを受け取るだけで済みます。しかし、複数のパラメーターを渡す必要がある場合、または一意のIDを生成するなど、説明した「準備」ロジックの一部を実行する必要がある場合はどうでしょうか?

`createSlice` を使用すると、reducerに「準備コールバック」を追加することで、これらの状況を処理できます。 `reducer` と `prepare` という名前の関数を持つオブジェクトを渡すことができます。生成されたアクションクリエーターを呼び出すと、`prepare` 関数が渡されたパラメーターで呼び出されます。次に、Flux Standard Action規則に一致する `payload` フィールド(または、オプションで `meta` および `error` フィールド)を持つオブジェクトを作成して返す必要があります。

ここでは、準備コールバックを使用して、`todoColorSelected` アクションクリエーターが個別の `todoId` と `color` 引数を受け取り、それらを `action.payload` のオブジェクトとしてまとめることができるようにしました。

一方、`todoDeleted` reducerでは、JSの `delete` 演算子を使用して、正規化された状態からアイテムを削除できます。

これらの同じパターンを使用して、`todosSlice.js` と `filtersSlice.js` の残りのreducerを書き直すことができます。

すべてのスライスが変換されたコードは次のようになります。

Thunkの記述

「読み込み中」、「リクエスト成功」、「リクエスト失敗」アクションをディスパッチするthunkを記述する方法を見てきました。これらのケースを処理するために、アクションクリエーター、アクションタイプ、*および* reducerを記述する必要がありました。

このパターンは非常に一般的であるため、**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` 呼び出しが、他の場所で定義された *他の* アクションタイプをリッスンする方法が必要です。

**`createSlice` は、`extraReducers` オプションも受け入れます。そこでは、同じスライスreducerが他のアクションタイプをリッスンできます**。このフィールドは、`builder` パラメーターを持つコールバック関数である必要があり、`builder.addCase(actionCreator, caseReducer)` を呼び出して他のアクションをリッスンできます。

ここで、builder.addCase(fetchTodos.pending, caseReducer) を呼び出しました。このアクションがディスパッチされると、state.status = 'loading' を設定するレデューサーが実行されます。これは、以前 switch 文でロジックを書いたときと同じです。fetchTodos.fulfilled に対しても同じことができ、API から受信したデータを処理できます。

もう1つの例として、saveNewTodo を変換してみましょう。このサンクは、新しい 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 について詳しく説明しませんが、参考までにいくつかの簡単な注意事項を挙げます。

  • サンクをディスパッチするときに、サンクに渡せる引数は1つだけです。複数の値を渡す必要がある場合は、単一のオブジェクトにまとめて渡します。
  • ペイロードクリエーターは、2番目の引数として {getState, dispatch} とその他の便利な値を含むオブジェクトを受け取ります。
  • サンクは、ペイロードクリエーターを実行する前に pending アクションをディスパッチし、返された Promise が成功したか失敗したかに基づいて fulfilled または rejected をディスパッチします。

状態の正規化

以前、アイテム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: セレクター関数の標準セットを生成します

todos スライスでこれらをどのように使用できるかを見てみましょう。

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 で受け取ります。「add」および「upsert」関数は単一のアイテムまたはアイテムの配列を受け取り、「remove」関数は単一のIDまたはIDの配列を受け取ります。

getInitialState を使用すると、含まれる追加の状態フィールドを渡すことができます。この場合、status フィールドを渡して、以前と同様に {ids, entities, status} の最終的な todos スライス状態を取得しました。

todos セレクター関数の一部も置き換えることができます。getSelectors アダプター関数は、すべてのアイテムの配列を返す selectAll や、1つのアイテムを返す selectById などのセレクターを生成します。ただし、getSelectors は Redux 状態ツリー全体のどこにデータがあるかを知らないため、状態ツリー全体からこのスライスを返す小さなセレクターを渡す必要があります。代わりにこれらを使用するように切り替えましょう。これはコードの最後の大きな変更なので、Redux Toolkit を使用したコードの最終バージョンを確認するために、今回は todos スライスファイル全体を含めます。

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 状態ツリー全体を受け取り、state.todos.entitiesstate.todos.ids をループして todo オブジェクトの完全な配列を提供する selectAll セレクターを生成します。selectAll は *何を* 選択しているかを教えてくれないため、分割代入構文を使用して関数の名前を selectTodos に変更できます。同様に、selectById の名前を selectTodoById に変更できます。

他のセレクターは、入力として selectTodos を引き続き使用していることに注意してください。これは、配列を state.todos 全体として保持していたか、ネストされた配列として保持していたか、正規化されたオブジェクトとして格納して配列に変換していたかに関係なく、todo オブジェクトの配列を返しているためです。データを格納する方法にこれらすべての変更を加えても、セレクターを使用することでコードの残りの部分を同じに保つことができ、メモ化されたセレクターを使用することで、不要な再レンダリングを回避して UI のパフォーマンスを向上させることができました。

学んだこと

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

これで、Redux とは何か、どのように機能するか、そして正しく使用する方法について、しっかりと理解できたはずです。

  • グローバルアプリ状態の管理
  • アプリの状態をプレーンな JS データとして保持する
  • アプリで「何が起こったか」を記述するアクションオブジェクトを作成する
  • 現在の状態とアクションを見て、それに応じて新しい状態を不変的に作成するレデューサー関数を使用する
  • useSelector を使用して React コンポーネントで Redux 状態を読み取る
  • useDispatch を使用して React コンポーネントからアクションをディスパッチする

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

情報

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

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

そして、このセクションで学んだ重要なポイントを最後にまとめてみましょう。

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

Redux を学習し、使用する次のステップ

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

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

しかし、「Redux Essentials」チュートリアルでは、**「現実世界」タイプのアプリケーションを構築する方法**を具体的に教えています。Redux Toolkit を使用して「Redux を正しく使用する方法」に焦点を当て、より大規模なアプリで見られるより現実的なパターンについて説明します。レデューサーが不変の更新を使用する必要がある理由など、この「基礎」チュートリアルと同じトピックの多くをカバーしますが、実際に動作するアプリケーションの構築に焦点を当てています。**次のステップとして、「Redux Essentials」チュートリアルを読むことを強くお勧めします。**

同時に、このチュートリアルで説明した概念は、React と Redux を使用して独自のアプリケーションの構築を開始するのに十分なはずです。これらの概念を固め、実際にどのように機能するかを確認するために、自分でプロジェクトに取り組んでみる良い機会です。どのようなプロジェクトを構築すればよいかわからない場合は、アプリプロジェクトのアイデアのリスト を参考にしてください。

Redux の使用 セクションには、レデューサーを構成する方法 など、いくつかの重要な概念に関する情報があり、スタイルガイドページ には、推奨されるパターンとベストプラクティスに関する重要な情報があります。

Redux がなぜ存在するのか、どのような問題を解決しようとしているのか、そしてどのように使用されることを意図しているのかについて詳しく知りたい場合は、Redux のメンテナーである Mark Erikson の投稿 Redux の道、パート 1: 実装と意図Redux の道、パート 2: 実践と哲学 をご覧ください。

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

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