本文へスキップ

RTK 2.0およびRedux 5.0への移行

学習内容
  • Redux Toolkit 2.0、Reduxコア5.0、Reselect 5.0、Redux Thunk 3.0での変更点(破壊的変更と新機能を含む)

はじめに

Redux Toolkitは2019年から利用可能であり、現在ではReduxアプリケーションを作成する標準的な方法です。4年以上、破壊的変更はありませんでした。RTK 2.0により、パッケージの近代化、非推奨オプションの整理、いくつかのエッジケースの強化が可能になります。

Redux Toolkit 2.0は、他のすべてのReduxパッケージのメジャーバージョン(Reduxコア5.0、React-Redux 9.0、Reselect 5.0、Redux Thunk 3.0)を伴います。.

このページでは、これらのパッケージにおける潜在的な破壊的変更と、Redux Toolkit 2.0の新機能を一覧表示します。念のためですが、**コアの`redux`パッケージを直接インストールまたは使用する必要はありません** - RTKはそれをラップし、すべてのメソッドと型を再エクスポートします。

実際には、**ほとんどの「破壊的」変更はエンドユーザーに実際の影響を与えることはなく、多くのプロジェクトではパッケージバージョンを更新するだけで、コードの変更はほとんど必要ないでしょう**。

アプリケーションコードの更新が必要となる可能性が高い変更点は次のとおりです。

パッケージングの変更(すべて)

すべてのRedux関連ライブラリのビルドパッケージングを更新しました。これらは技術的には「破壊的」ですが、エンドユーザーには透過的であるべきであり、NodeでESMファイル経由でReduxを使用するシナリオなどに対するより良いサポートを実現します。

`package.json`への`exports`フィールドの追加

どのアーティファクトをロードするかを定義するために`exports`フィールドを含むようにパッケージ定義を移行しました。主要なアーティファクトとして最新のESMビルドを使用し(互換性のためにCJSも含まれています)。

パッケージのローカルテストを実施しましたが、コミュニティの皆様にもご自身のプロジェクトで試していただき、見つかった問題を報告していただければ幸いです!

ビルドアーティファクトの近代化

ビルド出力をいくつかの方法で更新しました。

  • **ビルド出力はトランスパイルされなくなりました!** 代わりに、最新のJS構文(ES2020)をターゲットにしています。
  • すべてのビルドアーティファクトを`./dist/`下に移動しました。個別のトップレベルフォルダではなくなりました。
  • テスト対象の最低TypeScriptバージョンは、TS 4.7になりました。

UMDビルドの削除

Reduxは常にUMDビルドアーティファクトを付属していました。これらは主に、スクリプトタグとして直接インポートするためのものでした(CodePenやバンドラーを使用しないビルド環境など)。

現時点では、そのようなユースケースは現在かなりまれであるため、公開されたパッケージからそれらのビルドアーティファクトを削除しています。

`dist/$PACKAGE_NAME.browser.mjs`にブラウザ対応のESMビルドアーティファクトが含まれており、Unpkg上のそのファイルを参照するスクリプトタグを使用してロードできます。

UMDビルドアーティファクトを引き続き含めることに強い理由がある場合は、お知らせください!

破壊的変更

コア

アクション型は必ず文字列である必要があります

常にユーザーにアクションと状態は必ずシリアライズ可能であること、そして`action.type`は文字列であるべきであることを具体的に伝えてきました。これは、アクションがシリアライズ可能であることを保証し、Redux DevToolsで読み取り可能なアクション履歴を提供するためです。

`store.dispatch(action)`は、`action.type`が必ず文字列であることを具体的に強制するようになり、そうでない場合は、アクションがプレーンオブジェクトでない場合と同じようにエラーをスローします。

実際には、これは既に99.99%のケースで当てはまっており、ユーザーへの影響はないはずです(特にRedux Toolkitと`createSlice`を使用しているユーザー)。ただし、シンボルをアクション型として使用することを選択したレガシーReduxコードベースが存在する可能性があります。

`createStore`の非推奨化

Redux 4.2.0では、元の`createStore`メソッドを`@deprecated`とマークしました。厳密に言えば、これは破壊的変更ではありませんし、5.0でも新しいものではありませんが、完全性を期してここに記述しています。

この非推奨化は、ユーザーにレガシーReduxパターンからモダンRedux Toolkit APIを使用するようアプリケーションを移行することを促すための視覚的な指標にすぎません。.

非推奨化の結果、インポートして使用すると、createStoreのように実行時エラーや警告なしに視覚的に打ち消し線が引かれます

`createStore`は無限に動作し続け、決して削除されることはありません。しかし、今日はすべてのReduxユーザーに、すべてのReduxロジックにRedux Toolkitを使用してほしいと考えています。

これを修正するには、3つのオプションがあります。

  • Redux Toolkitと`configureStore`に切り替えることを強くお勧めします。
  • 何もしない。単なる視覚的な打ち消し線であり、コードの動作には影響しません。無視してください。
  • 現在エクスポートされている`legacy_createStore` APIを使用するように切り替えます。これはまったく同じ関数ですが、`@deprecated`タグがありません。最も簡単な方法は、`import { legacy_createStore as createStore } from 'redux'`のように、エイリアス付きインポートの名称変更を行うことです。

TypeScriptの書き換え

2019年、コミュニティ主導でReduxコードベースのTypeScriptへの変換を開始しました。#3500: TypeScriptへの移植で元の取り組みについて議論され、その作業はPR #3536: TypeScriptへの変換に統合されました。

しかし、既存のエコシステムとの互換性の問題(および私たちの側の一般的な慣性)に関する懸念により、TS変換されたコードは数年間にわたってリポジトリ内に未使用のまま公開されていませんでした。

Reduxコアv5は、そのTS変換されたソースコードからビルドされています。理論的には、これは実行時動作と型において4.xビルドとほぼ同一のはずですが、変更の一部が型の問題を引き起こす可能性が非常に高いです。

Githubで予期しない互換性の問題を報告してください!

`AnyAction`が`UnknownAction`に非推奨化されました

Redux TS型は常に`AnyAction`型をエクスポートしていました。これは`{type: string}`と定義され、他のフィールドを`any`として扱います。これにより`console.log(action.whatever)`のような使用が容易になりますが、残念ながら意味のある型安全性は提供されません。

現在、`UnknownAction`型をエクスポートしています。これは`action.type`以外のすべてのフィールドを`unknown`として扱います。これにより、ユーザーはアクションオブジェクトをチェックし、その特定のTS型をアサートする型ガードを記述することが推奨されます。これらのチェック内では、より良い型安全性を備えたフィールドにアクセスできます。

`UnknownAction`は、アクションオブジェクトを期待するReduxソース内のすべての場所でデフォルトになっています。

`AnyAction`はまだ互換性のために存在しますが、非推奨とマークされています。

Redux Toolkitのアクションクリエーターには、便利な型ガードとして機能する`.match()`メソッドがあります

if (todoAdded.match(someUnknownAction)) {
// action is now typed as a PayloadAction<Todo>
}

新しい`isAction`ユーティリティを使用して、不明な値が何らかのアクションオブジェクトであるかどうかを確認することもできます。

`Middleware`型が変更されました - Middlewareの`action`と`next`は`unknown`として型指定されます

以前は、nextパラメータは渡されたD型パラメータとして型付けされ、actionはdispatch型から抽出されたActionとして型付けされていました。しかし、これらはどちらも安全な想定ではありません。

  • nextは、チェーンの先頭にある、もはや適用されないものも含め、**すべての**dispatch拡張機能を持つように型付けされます。
    • 技術的には、基本的なReduxストアによって実装されたデフォルトのDispatchとしてnextを型付けするのはほぼ安全ですが、これによりnext(action)がエラーになります(actionが実際にActionであると約束できないため)。また、特定のアクションを見たときに渡されたアクション以外のものを返す後続のミドルウェアも考慮されません。
  • actionは必ずしも既知のアクションではありません。文字通り何でもかまいません。たとえば、thunkは.typeプロパティを持たない関数です(そのため、AnyActionは不正確です)。

next(action: unknown) => unknownに変更しました(これは正確で、nextが何を期待し、何を返すかはわかりません)。また、actionパラメータをunknownに変更しました(上記のように、これも正確です)。

action引数の値と相互作用したり、内部のフィールドにアクセスするには、まず型ガードチェックを実行して型を絞り込む必要があります(例:isAction(action)またはsomeActionCreator.match(action))。

この新しい型はv4のMiddleware型と互換性がありません。パッケージのミドルウェアが互換性が無いと言っている場合は、どのバージョンのReduxから型を取得しているかを確認してください!(このページの後の方にある依存関係のオーバーライドを参照してください。)

PreloadedState型は、Reducerジェネリックに置き換えられました

型安全と動作を向上させるため、TS型の微調整を行いました。

まず、Reducer型には、PreloadedStateという可能性のあるジェネリックが追加されました。

type Reducer<S, A extends Action, PreloadedState = S> = (
state: S | PreloadedState | undefined,
action: A
) => S

#4491の説明に従って

なぜこの変更が必要なのか?ストアがcreateStore/configureStoreによって最初に作成されるとき、初期状態はpreloadedState引数として渡されたもの(何も渡されなければundefined)に設定されます。つまり、Reducerが最初に呼び出されるとき、それはpreloadedStateを使用して呼び出されます。最初の呼び出しの後、Reducerには常に現在の状態(S)が渡されます。

ほとんどの通常のReducerでは、S | undefinedpreloadedStateに渡すことができるものを正確に記述しています。しかし、combineReducers関数はPartial | undefinedのプリロード状態を許可します。

解決策は、Reducerがプリロード状態として受け入れるものを表す別のジェネリックを持つことです。そうすれば、createStoreはそのジェネリックをpreloadedState引数に使用できます。

以前は、これは$CombinedState型によって処理されていましたが、これは事態を複雑にし、ユーザーからいくつかの問題が報告されました。これにより、$CombinedStateの必要性が完全に解消されます。

この変更にはいくつかの破壊的変更が含まれていますが、全体的にはユーザーランドでアップグレードするユーザーへの影響はそれほど大きくありません。

  • ReducerReducersMapObject、およびcreateStore/configureStoreの型/関数は、Sをデフォルト値とする追加のPreloadedStateジェネリックを受け入れます。
  • combineReducersのオーバーロードは、ReducersMapObjectをジェネリックパラメータとして取る単一の関数定義に置き換えられました。オーバーロードの削除はこれらの変更で必要でした。なぜなら、場合によっては間違ったオーバーロードを選択していたためです。
  • Reducerのジェネリックを明示的にリストするエンハンサーは、3番目のジェネリックを追加する必要があります。

ツールキットのみ

createSlice.extraReducerscreateReducerのオブジェクト構文が削除されました

RTKのcreateReducer APIは、当初、アクションタイプの文字列のルックアップテーブルをケースReducerに受け入れるように設計されていました(例:{ "ADD_TODO": (state, action) => {} })。その後、「マッチャー」とデフォルトのハンドラーを追加する柔軟性を高めるために「ビルダーコールバック」形式を追加し、createSlice.extraReducersについても同様の処理を行いました。

RTK 2.0では、createReducercreateSlice.extraReducersの両方について「オブジェクト」形式を削除しました。ビルダーコールバック形式は事実上同じ行数のコードであり、TypeScriptと連携して動作するからです。

例として、これ

const todoAdded = createAction('todos/todoAdded')

createReducer(initialState, {
[todoAdded]: (state, action) => {}
})

createSlice({
name,
initialState,
reducers: {
/* case reducers here */
},
extraReducers: {
[todoAdded]: (state, action) => {}
}
})

はこれに移行する必要があります。

createReducer(initialState, builder => {
builder.addCase(todoAdded, (state, action) => {})
})

createSlice({
name,
initialState,
reducers: {
/* case reducers here */
},
extraReducers: builder => {
builder.addCase(todoAdded, (state, action) => {})
}
})
コードモッド

コードベースのアップグレードを簡素化するために、非推奨の「オブジェクト」構文を同等の「ビルダー」構文に自動的に変換するコードモッドのセットを公開しました。

コードモッドパッケージは、@reduxjs/rtk-codemodsとしてNPMで利用できます。詳細はこちらをご覧ください。

コードベースに対してコードモッドを実行するには、npx @reduxjs/rtk-codemods <TRANSFORM NAME> path/of/files/ or/some**/*glob.jsを実行します。

npx @reduxjs/rtk-codemods createReducerBuilder ./src

npx @reduxjs/rtk-codemods createSliceBuilder ./packages/my-app/**/*.ts

変更をコミットする前に、コードベースでPrettierを再実行することをお勧めします。

これらのコードモッドは機能するはずですが、より現実世界のコードベースからのフィードバックをいただければ幸いです!

configureStore.middlewareはコールバックである必要があります

当初から、configureStoremiddlewareオプションとして直接配列値を受け入れていました。しかし、配列を直接提供すると、configureStoregetDefaultMiddleware()を呼び出すことができなくなります。そのため、middleware: [myMiddleware]は、thunkミドルウェア(または開発モードチェック)が追加されていないことを意味します。

これは危険な落とし穴であり、多くのユーザーが誤ってこれを行い、デフォルトのミドルウェアが構成されなかったためにアプリが失敗するということがありました。

その結果、middlewareはコールバック形式のみを受け入れるようになりました。何らかの理由ですべての組み込みミドルウェアを置き換えたい場合は、コールバックから配列を返すことで行います。

const store = configureStore({
reducer,
middleware: getDefaultMiddleware => {
// WARNING: this means that _none_ of the default middleware are added!
return [myMiddleware]
// or for TS users, use:
// return new Tuple(myMiddleware)
}
})

しかし、デフォルトのミドルウェアを完全に置き換えることは一貫して推奨されていませんreturn getDefaultMiddleware().concat(myMiddleware)を使用する必要があります。

configureStore.enhancersはコールバックである必要があります

configureStore.middlewareと同様に、enhancersフィールドも、同じ理由でコールバックである必要があります。

コールバックは、バッチングエンハンサーをカスタマイズするために使用できるgetDefaultEnhancers関数を取得しますこれはデフォルトで含まれるようになりました

例:

const store = configureStore({
reducer,
enhancers: getDefaultEnhancers => {
return getDefaultEnhancers({
autoBatch: { type: 'tick' }
}).concat(myEnhancer)
}
})

getDefaultEnhancersの結果には、構成された/デフォルトのミドルウェアで作成されたミドルウェアエンハンサーも含まれます。間違いを防ぐために、ミドルウェアが提供されていて、ミドルウェアエンハンサーがコールバックの結果に含まれていない場合、configureStoreはコンソールにエラーをログ出力します。

const store = configureStore({
reducer,
enhancers: getDefaultEnhancers => {
return [myEnhancer] // we've lost the middleware here
// instead:
return getDefaultEnhancers().concat(myEnhancer)
}
})

スタンドアロンのgetDefaultMiddlewaregetTypeが削除されました

スタンドアロンバージョンのgetDefaultMiddlewareはv1.6.1以降非推奨になっており、現在は削除されました。代わりに、正しい型を持つmiddlewareコールバックに渡された関数を使用してください。

createActionで作成されたアクションクリエーターから型文字列を抽出するために使用されていたgetTypeエクスポートも削除しました。代わりに、静的プロパティactionCreator.typeを使用してください。

RTK Queryの動作変更

dispatch(endpoint.initiate(arg, {subscription: false}))の使用に関して、RTK Queryに問題があるという報告が多数ありました。また、複数のトリガーされた遅延クエリが間違ったタイミングでプロミスを解決するという報告もありました。これらはどちらも、RTKQがこれらのケースでキャッシュエントリを追跡していなかった(意図的に)という同じ根本的な問題がありました。キャッシュエントリを常に追跡し(必要に応じて削除し)、これらの動作上の問題を解決するロジックを改良しました。

また、複数のミューテーションを連続して実行しようとした場合と、タグの無効化の動作に関する問題も提起されました。RTKQには、複数の無効化をまとめて処理できるように、タグの無効化を短時間遅らせる内部ロジックが追加されました。これは、createApiの新しいinvalidationBehavior: 'immediate' | 'delayed'フラグによって制御されます。新しいデフォルトの動作は'delayed'です。RTK 1.9の動作に戻すには、'immediate'に設定します。

RTK 1.9では、RTK Queryの内部を改良し、ほとんどのサブスクリプションの状態をRTKQミドルウェア内に保持するようにしました。値は依然としてReduxストアの状態に同期されますが、これは主にRedux DevToolsの「RTK Query」パネルによる表示のためです。上記のキャッシュエントリの変更に関連して、パフォーマンスのために、これらの値がRedux状態に同期される頻度を最適化しました。

reactHooksModuleカスタムフックの設定

以前は、React Reduxのフック(useSelectoruseDispatchuseStore)のカスタムバージョンをreactHooksModuleに個別に渡すことができました。これは通常、デフォルトのReactReduxContextとは異なるコンテキストを使用できるようにするためです。

実際には、reactフックモジュールはこれらの3つのフックすべてを提供する必要があり、useStoreなしでuseSelectoruseDispatchのみを渡すという簡単な間違いになりやすくなっていました。

モジュールはこれらの3つすべてを同じ構成キーの下に移動し、キーが存在する場合は、すべてが提供されていることをチェックします。

// previously
const customCreateApi = buildCreateApi(
coreModule(),
reactHooksModule({
useDispatch: createDispatchHook(MyContext),
useSelector: createSelectorHook(MyContext),
useStore: createStoreHook(MyContext)
})
)

// now
const customCreateApi = buildCreateApi(
coreModule(),
reactHooksModule({
hooks: {
useDispatch: createDispatchHook(MyContext),
useSelector: createSelectorHook(MyContext),
useStore: createStoreHook(MyContext)
}
})
)

エラーメッセージの抽出

Redux 4.1.0は、Reactのアプローチに基づいて本番ビルドからエラーメッセージ文字列を抽出することで、バンドルサイズを最適化しました。同じ手法をRTKに適用しました。これにより、本番バンドルから約1000バイト削減されます(実際の利点は、どのインポートが使用されているかによって異なります)。

configureStoremiddlewareのフィールド順序が重要です

middlewareenhancersの両方のフィールドをconfigureStoreに渡す場合、内部TSの推論が正しく機能するためには、middlewareフィールドが最初に来る必要があります。

デフォルト以外のミドルウェア/エンハンサーはTupleを使用する必要があります

middlewareパラメータをconfigureStoreに渡しているユーザーが、getDefaultMiddleware()によって返された配列をスプレッドしようとしたり、別のプレーン配列を渡そうとした多くのケースが見られました。残念ながら、これにより個々のミドルウェアからの正確なTS型が失われ、多くの場合、後でTSの問題が発生します(例:dispatchDispatchとして型付けされ、thunkについて認識しない)。

getDefaultMiddleware()はすでに内部のMiddlewareArrayクラス(Arrayのサブクラスで、ミドルウェアの型を正しく取得して保持する.concat/prepend()メソッドが強く型付けされている)を使用していました。

この型をTupleに名前変更し、configureStoreのTS型では、独自のミドルウェア配列を渡す場合はTupleを使用する必要があります。

import { configureStore, Tuple } from '@reduxjs/toolkit'

configureStore({
reducer: rootReducer,
middleware: getDefaultMiddleware => new Tuple(additionalMiddleware, logger)
})

(プレーンなJSでRTKを使用している場合は、これには影響しません。プレーンな配列を渡すことができます。)

この同じ制限は、enhancersフィールドにも適用されます。

エンティティアダプターの型の更新

createEntityAdapter には、ID の型を厳密に指定するためのジェネリック引数 Id が追加されました。これにより、ID が公開される場所であればどこでも、ID の型を正確に指定できるようになります。以前は、ID フィールドの型は常に string | number でした。TS は、エンティティ型の .id フィールド、または selectId の戻り値の型から正確な型を推論しようとします。このジェネリック型を直接渡すこともできます。**EntityState<Data, Id> 型を直接使用する場合は、両方のジェネリック引数を必ず指定する必要があります!**

.entities ルックアップテーブルは、標準的な TS Record<Id, MyEntityType> を使用するように定義されました。これは、各項目のルックアップがデフォルトで存在することを前提としています。以前は、Dictionary<MyEntityType> 型を使用しており、結果は MyEntityType | undefined と仮定していました。Dictionary 型は削除されました。

ルックアップが未定義になる可能性があると想定したい場合は、TypeScript の noUncheckedIndexedAccess 構成オプションを使用して制御できます。

Reselect

createSelector はデフォルトのメモ化関数として weakMapMemoize を使用する

**createSelector は、weakMapMemoize という新しいデフォルトのメモ化関数を使用するようになりました。**このメモ化関数は、事実上無限のキャッシュサイズを提供するため、可変の引数を使用する場合でも使いやすくなりますが、参照比較のみに依存します。

等価比較をカスタマイズする必要がある場合は、元の lruMemoize メソッドを使用するように createSelector をカスタマイズしてください。

createSelector(inputs, resultFn, {
memoize: lruMemoize,
memoizeOptions: { equalityCheck: yourEqualityFunction }
})

defaultMemoizelruMemoize に名前変更

元の defaultMemoize 関数はもはやデフォルトではないため、明確にするために lruMemoize に名前を変更しました。これは、セレクタのカスタマイズのためにアプリケーションにこれを特にインポートした場合にのみ重要です。

createSelector の開発モードチェック

createSelector は、開発モードで、常に新しい参照を返す入力セレクタや、引数をすぐに返す結果関数など、一般的な間違いをチェックするようになりました。これらのチェックは、セレクタの作成時またはグローバルにカスタマイズできます。

これは重要です。同じパラメータで本質的に異なる結果を返す入力セレクタは、出力セレクタが正しくメモ化されず、不要に実行されることを意味し、そのため(可能性として)新しい結果が作成され、再レンダリングが発生します。

const addNumbers = createSelector(
// this input selector will always return a new reference when run
// so cache will never be used
(a, b) => ({ a, b }),
({ a, b }) => ({ total: a + b })
)
// instead, you should have an input selector for each stable piece of data
const addNumbersStable = createSelector(
(a, b) => a,
(a, b) => b,
(a, b) => ({
total: a + b
})
)

これは、特に設定されていない限り、セレクタが初めて呼び出されたときに実行されます。詳細については、Reselect の開発モードチェックに関するドキュメントを参照してください。

RTK は createSelector を再エクスポートしますが、このチェックをグローバルに構成する関数を意図的に再エクスポートしていないことに注意してください。グローバルに構成したい場合は、reselect に直接依存し、自分でインポートする必要があります。

ParametricSelector 型の削除

ParametricSelector 型と OutputParametricSelector 型は削除されました。代わりに SelectorOutputSelector を使用してください。

React-Redux

React 18 が必要

React-Redux v7 および v8 は、フックをサポートするすべてのバージョンの React(16.8 以降、17、および 18)で動作しました。v8 は内部のサブスクリプション管理から React の新しい useSyncExternalStore フックに切り替えましたが、React 16.8 および 17(このフックが組み込まれていない)をサポートするために「shim」実装を使用していました。

React-Redux v9 は React 18 を必須とし、React 16 または 17 をサポートしませんこれにより、shim を削除し、バンドルサイズを少し削減できます。

Redux Thunk

Thunk は名前付きエクスポートを使用

redux-thunk パッケージは以前、ミドルウェアである単一のデフォルトエクスポートを使用しており、カスタマイズを可能にする withExtraArgument という名前のフィールドが添付されていました。

デフォルトエクスポートは削除されました。現在は、thunk(基本的なミドルウェア)と withExtraArgument の 2 つの名前付きエクスポートがあります。

Redux Toolkit を使用している場合は、RTK が configureStore の内部で既にこれらを処理するため、影響はありません。

新機能

これらの機能は Redux Toolkit 2.0 の新機能であり、エコシステムでユーザーから求められていた追加のユースケースに対応するのに役立ちます。

コード分割のためのスライスリデューサインジェクション付き combineSlices API

Redux コアには常に combineReducers が含まれており、これは「スライスリデューサ」関数のオブジェクトを受け取り、それらのスライスリデューサを呼び出すリデューサを生成します。RTK の createSlice はスライスリデューサと関連するアクションクリエータを生成し、個々のアクションクリエータを名前付きエクスポートとして、スライスリデューサをデフォルトエクスポートとしてエクスポートするパターンを学習しました。一方、リデューサの遅延読み込みは公式にはサポートされていませんでしたが、ドキュメントにはいくつかの「リデューサインジェクション」パターンのサンプルコードがあります。

このリリースには、ランタイムでのリデューサの遅延読み込みを有効にするように設計された新しいcombineSlices API が含まれています。これは、個々のスライスまたはスライスのオブジェクトを引数として受け取り、sliceObject.name フィールドを各状態フィールドのキーとして使用して combineReducers を自動的に呼び出します。生成されたリデューサ関数には、ランタイムで追加のスライスを動的にインジェクトするために使用できる追加の .inject() メソッドが添付されています。また、後で追加されるリデューサの TS 型を生成するために使用できる .withLazyLoadedSlices() メソッドも含まれています。#2776 を参照してください(このアイデアに関する元の議論)。

現時点では、これを configureStore に組み込んでいないため、自分で const rootReducer = combineSlices(.....) を呼び出し、それを configureStore({reducer: rootReducer}) に渡す必要があります。

基本的な使用方法:スライスとスタンドアロンリデューサの組み合わせを combineSlices に渡す

const stringSlice = createSlice({
name: 'string',
initialState: '',
reducers: {}
})

const numberSlice = createSlice({
name: 'number',
initialState: 0,
reducers: {}
})

const booleanReducer = createReducer(false, () => {})

const api = createApi(/* */)

const combinedReducer = combineSlices(
stringSlice,
{
num: numberSlice.reducer,
boolean: booleanReducer
},
api
)
expect(combinedReducer(undefined, dummyAction())).toEqual({
string: stringSlice.getInitialState(),
num: numberSlice.getInitialState(),
boolean: booleanReducer.getInitialState(),
api: api.reducer.getInitialState()
})

基本的なスライスリデューサインジェクション

// Create a reducer with a TS type that knows `numberSlice` will be injected
const combinedReducer =
combineSlices(stringSlice).withLazyLoadedSlices<
WithSlice<typeof numberSlice>
>()

// `state.number` doesn't exist initially
expect(combinedReducer(undefined, dummyAction()).number).toBe(undefined)

// Create a version of the reducer with `numberSlice` injected (mainly useful for types)
const injectedReducer = combinedReducer.inject(numberSlice)

// `state.number` now exists, and injectedReducer's type no longer marks it as optional
expect(injectedReducer(undefined, dummyAction()).number).toBe(
numberSlice.getInitialState()
)

// original reducer has also been changed (type is still optional)
expect(combinedReducer(undefined, dummyAction()).number).toBe(
numberSlice.getInitialState()
)

createSliceselectors フィールド

既存の createSlice API は、selectors をスライスの部分として直接定義するサポートを持つようになりました。デフォルトでは、これらのセレクタは、slice.name をフィールドとして使用してルート状態にスライスがマウントされていると仮定して生成されます(例:name: "todos" -> rootState.todos)。さらに、そのデフォルトのルート状態ルックアップを行う slice.selectSlice メソッドもあります。

entityAdapter.getSelectors() の動作と同様に、別の場所を使用してセレクタを生成するには、sliceObject.getSelectors(selectSliceState) を呼び出すことができます。

const slice = createSlice({
name: 'counter',
initialState: 42,
reducers: {},
selectors: {
selectSlice: state => state,
selectMultiple: (state, multiplier: number) => state * multiplier
}
})

// Basic usage
const testState = {
[slice.name]: slice.getInitialState()
}
const { selectSlice, selectMultiple } = slice.selectors
expect(selectSlice(testState)).toBe(slice.getInitialState())
expect(selectMultiple(testState, 2)).toBe(slice.getInitialState() * 2)

// Usage with the slice reducer mounted under a different key
const customState = {
number: slice.getInitialState()
}
const { selectSlice, selectMultiple } = slice.getSelectors(
(state: typeof customState) => state.number
)
expect(selectSlice(customState)).toBe(slice.getInitialState())
expect(selectMultiple(customState, 2)).toBe(slice.getInitialState() * 2)

createSlice.reducers のコールバック構文と thunk のサポート

最も古い機能リクエストの 1 つは、createSlice の内部で thunk を直接宣言する機能です。これまでは、常に個別に宣言し、thunk に文字列アクションプレフィックスを与え、createSlice.extraReducers を介してアクションを処理する必要がありました。

// Declare the thunk separately
const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async (userId: number, thunkAPI) => {
const response = await userAPI.fetchById(userId)
return response.data
}
)

const usersSlice = createSlice({
name: 'users',
initialState,
reducers: {
// standard reducer logic, with auto-generated action types per reducer
},
extraReducers: builder => {
// Add reducers for additional action types here, and handle loading state as needed
builder.addCase(fetchUserById.fulfilled, (state, action) => {
state.entities.push(action.payload)
})
}
})

多くのユーザーが、この分離が不自然だと感じていると伝えています。

createSlice の内部で thunk を定義する方法を含めたいと思っていましたが、さまざまなプロトタイプを試していました。常に 2 つの大きな問題と、副次的な懸念事項がありました。

  1. 内部で thunk を宣言するための構文が明確ではありませんでした。
  2. Thunk は getStatedispatch にアクセスできますが、RootStateAppDispatch の型は通常ストアから推論され、ストアはスライス状態の型から推論します。createSlice の内部で thunk を宣言すると、ストアはスライスの型を必要とする一方で、スライスはストアの型を必要とするため、循環的な型推論エラーが発生します。JS ユーザーには問題なく動作するが TS ユーザーには動作しない API を出荷することは避けたいと考えていました。特に、RTK で TS を使用してほしいと考えているからです。
  3. ES モジュールでは同期的な条件付きインポートを行うことができず、createAsyncThunk のインポートをオプションにする良い方法がありません。createSlice が常にそれに依存する(そしてそれをバンドルサイズに追加する)か、createAsyncThunk をまったく使用できなくなります。

私たちはこれらの妥協案に落ち着きました。

  • createSlice で非同期 thunk を作成するには、createAsyncThunk にアクセスできるcreateSlice のカスタムバージョンを設定する必要があります。.
  • RTK Query の createApibuild コールバック構文と同様の「クリエータコールバック」構文を使用して、createSlice.reducers の内部で thunk を宣言できます(オブジェクト内のフィールドを作成するための型付き関数を使用)。これは、reducers フィールドの既存の「オブジェクト」構文とは少し異なりますが、かなり似ています。
  • createSlice の内部にある thunk の型の一部はカスタマイズできますが、statedispatch の型はカスタマイズできません。それらが必要な場合は、getState() as RootState のように手動で as キャストを行うことができます。

実際には、これらは妥当な妥協案であると期待しています。createSlice の内部で thunk を作成することは広く求められていたため、使用される API になると考えています。TS のカスタマイズオプションが制限である場合は、常に createSlice の外部で thunk を宣言できます。ほとんどの非同期 thunk は dispatchgetState を必要としません。データを取得して返すだけです。最後に、カスタム createSlice を設定することで、createAsyncThunk をバンドルサイズに含めることを選択できます(直接使用する場合、または RTK Query の一部として既に含まれている可能性があります。いずれの場合も、追加のバンドルサイズは発生しません)。

新しいコールバック構文の例を以下に示します。

const createSliceWithThunks = buildCreateSlice({
creators: { asyncThunk: asyncThunkCreator }
})

const todosSlice = createSliceWithThunks({
name: 'todos',
initialState: {
loading: false,
todos: [],
error: null
} as TodoState,
reducers: create => ({
// A normal "case reducer", same as always
deleteTodo: create.reducer((state, action: PayloadAction<number>) => {
state.todos.splice(action.payload, 1)
}),
// A case reducer with a "prepare callback" to customize the action
addTodo: create.preparedReducer(
(text: string) => {
const id = nanoid()
return { payload: { id, text } }
},
// action type is inferred from prepare callback
(state, action) => {
state.todos.push(action.payload)
}
),
// An async thunk
fetchTodo: create.asyncThunk(
// Async payload function as the first argument
async (id: string, thunkApi) => {
const res = await fetch(`myApi/todos?id=${id}`)
return (await res.json()) as Item
},
// An object containing `{pending?, rejected?, fulfilled?, settled?, options?}` second
{
pending: state => {
state.loading = true
},
rejected: (state, action) => {
state.error = action.payload ?? action.error
},
fulfilled: (state, action) => {
state.todos.push(action.payload)
},
// settled is called for both rejected and fulfilled actions
settled: (state, action) => {
state.loading = false
}
}
)
})
})

// `addTodo` and `deleteTodo` are normal action creators.
// `fetchTodo` is the async thunk
export const { addTodo, deleteTodo, fetchTodo } = todosSlice.actions

Codemod

新しいコールバック構文の使用は完全にオプションです(オブジェクト構文はまだ標準です)が、既存のスライスはこの構文が提供する新しい機能を利用するには、変換する必要があります。これを容易にするために、codemod が提供されています。

npx @reduxjs/rtk-codemods createSliceReducerBuilder ./src/features/todos/slice.ts

「動的ミドルウェア」ミドルウェア

Redux ストアの中間ウェアパイプラインは、ストアの作成時に固定され、後で変更できません。コード分割など、動的に中間ウェアを追加および削除しようとするエコシステムライブラリも存在しています。

これは比較的ニッチなユースケースですが、独自の「動的ミドルウェア」ミドルウェアのバージョンを構築しました。セットアップ時に Redux ストアに追加すると、後でランタイムで中間ウェアを追加できます。また、ストアに中間ウェアを自動的に追加し、更新されたディスパッチメソッドを返す React フック統合も含まれています。

import { createDynamicMiddleware, configureStore } from '@reduxjs/toolkit'

const dynamicMiddleware = createDynamicMiddleware()

const store = configureStore({
reducer: {
todos: todosReducer
},
middleware: getDefaultMiddleware =>
getDefaultMiddleware().prepend(dynamicMiddleware.middleware)
})

// later
dynamicMiddleware.addMiddleware(someOtherMiddleware)

configureStore はデフォルトで autoBatchEnhancer を追加

v1.9.0では、新しい`autoBatchEnhancer`を追加しました。

これは、複数の「低優先度」アクションが連続してディスパッチされた場合、サブスクライバーへの通知を短時間遅延させます。UIの更新は通常、更新プロセスの最もコストのかかる部分であるため、これによりパフォーマンスが向上します。RTK Queryは、デフォルトでほとんどの内部アクションを「低優先度」としてマークしますが、その恩恵を受けるには、ストアに`autoBatchEnhancer`を追加する必要があります。

ユーザーがストアの設定を手動で調整する必要なく、パフォーマンス向上を享受できるように、`configureStore`を更新して、`autoBatchEnhancer`をストア設定にデフォルトで追加しました。

`entityAdapter.getSelectors`は、`createSelector`関数を引数として受け入れます

`entityAdapter.getSelectors()`は、2番目の引数としてオプションオブジェクトを受け入れるようになりました。これにより、生成されたセレクターをメモ化するために使用する独自の`createSelector`メソッドを渡すことができます。これは、Reselectの新しい代替メモイザーの1つ、または同等のシグネチャを持つ他のメモ化ライブラリを使用する場合に役立ちます。

Immer 10.0

Immer 10.0が正式リリースされました。いくつかの主要な改善とアップデートが含まれています。

  • 大幅な更新パフォーマンスの向上
  • バンドルサイズの縮小
  • ESM/CJSパッケージフォーマットの改善
  • デフォルトエクスポートの廃止
  • ES5フォールバックの廃止

RTKを最終版のImmer 10.0リリースに依存するように更新しました。

Next.js設定ガイド

Next.jsでReduxを適切に設定する方法を網羅したドキュメントページを用意しました。Redux、Next、App Routerを一緒に使用することについての多くの質問を受けており、このガイドはアドバイスを提供するのに役立つはずです。

(現時点では、Next.jsの`with-redux`の例はまだ古いパターンを示しています。すぐにPRを提出して、ドキュメントガイドに合わせます。)

依存関係の上書き

パッケージがReduxコア5.0を許可するようにピア依存関係を更新するまでには時間がかかり、その間、ミドルウェアの種類のような変更により、非互換性と認識される問題が発生します。

ほとんどのライブラリは、実際には5.0と非互換性のあるプラクティスは持っていませんが、4.0へのピア依存関係のため、古い型宣言を取り込んでしまいます。

これは、`npm`と`yarn`の両方でサポートされている依存関係解決を手動で上書きすることで解決できます。

`npm` - `overrides`

NPMは、`package.json`の`overrides`フィールドを介してこれをサポートしています。特定のパッケージの依存関係を上書きするか、Reduxを取り込むすべてのパッケージが同じバージョンを受け取るようにすることができます。

個々の上書き - redux-persist
{
"overrides": {
"redux-persist": {
"redux": "^5.0.0"
}
}
}
一括上書き
{
"overrides": {
"redux": "^5.0.0"
}
}

`yarn` - `resolutions`

Yarnは、`package.json`の`resolutions`フィールドを介してこれをサポートしています。NPMと同様に、特定のパッケージの依存関係を上書きするか、Reduxを取り込むすべてのパッケージが同じバージョンを受け取るようにすることができます。

個々の上書き - redux-persist
{
"resolutions": {
"redux-persist/redux": "^5.0.0"
}
}
一括上書き
{
"resolutions": {
"redux": "^5.0.0"
}
}

推奨事項

2.0と以前のバージョンでの変更に基づいて、重要ではないにしても知っておくと良い考え方の一部が変化しています。

`actionCreator.toString()`の代替手段

RTKの元のAPIの一部として、`createAction`で作成されたアクションクリエーターには、アクションタイプを返すカスタム`toString()`オーバーライドがあります。

これは主に、(現在は削除済み)`createReducer`のオブジェクト構文で使用されました。

const todoAdded = createAction<Todo>('todos/todoAdded')

createReducer(initialState, {
[todoAdded]: (state, action) => {} // toString called here, 'todos/todoAdded'
})

これは便利でしたが(`redux-saga`や`redux-observable`などのReduxエコシステム内の他のライブラリは、さまざまな機能でこれをサポートしていました)、Typescriptと連携せず、一般的に少し「魔法」が多すぎました。

const test = todoAdded.toString()
// ^? typed as string, rather than specific action type

時間の経過とともに、アクションクリエーターは、より明示的でTypescriptとより良く連携する静的な`type`プロパティと`match`メソッドも獲得しました。

const test = todoAdded.type
// ^? 'todos/todoAdded'

// acts as a type predicate
if (todoAdded.match(unknownAction)) {
unknownAction.payload
// ^? now typed as PayloadAction<Todo>
}

互換性のために、このオーバーライドはまだ存在しますが、より分かりやすいコードのために、静的プロパティのいずれかを使用することをお勧めします。

たとえば、`redux-observable`の場合

// before (works in runtime, will not filter types properly)
const epic = (action$: Observable<Action>) =>
action$.pipe(
ofType(todoAdded),
map(action => action)
// ^? still Action<any>
)

// consider (better type filtering)
const epic = (action$: Observable<Action>) =>
action$.pipe(
filter(todoAdded.match),
map(action => action)
// ^? now PayloadAction<Todo>
)

`redux-saga`の場合

// before (still works)
yield takeEvery(todoAdded, saga)

// consider
yield takeEvery(todoAdded.match, saga)
// or
yield takeEvery(todoAdded.type, saga)

今後の計画

カスタムスライスReducerクリエーター

`createSlice`のコールバック構文の追加により、カスタムスライスReducerクリエーターを有効にするという提案がなされました。これらのクリエーターは、以下を行うことができます。

  • ケースまたはマッチャーReducerを追加することで、Reducerの動作を変更する
  • `slice.actions`にアクション(またはその他の便利な関数)をアタッチする
  • `slice.caseReducers`に提供されたケースReducerをアタッチする

クリエーターは、最初に`createSlice`が呼び出されたときに「定義」シェイプを返す必要があります。その後、必要なReducerやアクションを追加することで処理します。

このためのAPIはまだ確定していませんが、潜在的なAPIを使用して実装された既存の`create.asyncThunk`クリエーターは次のようになります。

const asyncThunkCreator = {
type: ReducerType.asyncThunk,
define(payloadCreator, config) {
return {
type: ReducerType.asyncThunk, // needs to match reducer type, so correct handler can be called
payloadCreator,
...config
}
},
handle(
{
// the key the reducer was defined under
reducerName,
// the autogenerated action type, i.e. `${slice.name}/${reducerName}`
type
},
// the definition from define()
definition,
// methods to modify slice
context
) {
const { payloadCreator, options, pending, fulfilled, rejected, settled } =
definition
const asyncThunk = createAsyncThunk(type, payloadCreator, options)

if (pending) context.addCase(asyncThunk.pending, pending)
if (fulfilled) context.addCase(asyncThunk.fulfilled, fulfilled)
if (rejected) context.addCase(asyncThunk.rejected, rejected)
if (settled) context.addMatcher(asyncThunk.settled, settled)

context.exposeAction(reducerName, asyncThunk)
context.exposeCaseReducer(reducerName, {
pending: pending || noop,
fulfilled: fulfilled || noop,
rejected: rejected || noop,
settled: settled || noop
})
}
}

const createSlice = buildCreateSlice({
creators: {
asyncThunk: asyncThunkCreator
}
})

しかし、実際にこれを使用する人やライブラリの数については不確かであるため、Githubのissueへのフィードバックを歓迎します!

`createSlice.selector`セレクターファクトリ

`createSlice.selectors`がメモ化されたセレクターを十分にサポートしているかどうかについて、内部的に懸念事項が提起されています。メモ化されたセレクターを`createSlice.selectors`構成に提供できますが、その1つのインスタンスに限定されます。

const todoSlice = createSlice({
name: 'todos',
initialState: {
todos: [] as Todo[]
},
reducers: {},
selectors: {
selectTodosByAuthor = createSelector(
(state: TodoState) => state.todos,
(state: TodoState, author: string) => author,
(todos, author) => todos.filter(todo => todo.author === author)
)
}
})

export const { selectTodosByAuthor } = todoSlice.selectors

`createSelector`のデフォルトのキャッシュサイズは1であるため、異なる引数を持つ複数のコンポーネントで呼び出された場合、キャッシングの問題が発生する可能性があります。これに対する一般的なソリューション(`createSlice`なし)はセレクターファクトリです。

export const makeSelectTodosByAuthor = () =>
createSelector(
(state: RootState) => state.todos.todos,
(state: RootState, author: string) => author,
(todos, author) => todos.filter(todo => todo.author === author)
)

function AuthorTodos({ author }: { author: string }) {
const selectTodosByAuthor = useMemo(makeSelectTodosByAuthor, [])
const todos = useSelector(state => selectTodosByAuthor(state, author))
}

もちろん、`createSlice.selectors`では、スライスを作成する際にセレクターインスタンスが必要になるため、これは不可能です。

2.0.0では、これに対する解決策はまだ設定されていません。いくつかのAPIが提案されています(PR 1PR 2)が、決定されたものは何もありません。これをサポートしてほしい場合は、Githubのディスカッションでフィードバックを提供してください!

3.0 - RTK Query

RTK 2.0は主にコアとツールキットの変更に焦点を当てていました。2.0がリリースされたので、まだ解決されていない問題があるため(そのいくつかは破壊的変更を必要とする可能性があり、3.0リリースが必要になります)、RTK Queryに焦点を移したいと思います。

それについてどのようなものになるかについてのフィードバックがある場合は、RTK Query APIの課題と問題点に関するフィードバックのスレッドに参加してください!