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はそれをラップし、すべてのメソッドと型を再エクスポートします。
実際には、**ほとんどの「破壊的」な変更はエンドユーザーに実際の影響を与えることはなく、多くのプロジェクトではパッケージのバージョンを更新するだけで、コードの変更はほとんど必要ないことを期待しています**。
アプリコードの更新が必要になる可能性が最も高い変更点は次のとおりです。
createReducer
とcreateSlice.extraReducers
でオブジェクト構文が削除されました。configureStore.middleware
はコールバックである必要があります。Middleware
型が変更されました - Middlewareのaction
とnext
はunknown
として型付けされます。
パッケージングの変更(すべて)
すべての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
に切り替えるという強力な提案に従ってください。 - 何もせずに放置してください。視覚的に打ち消し線が引かれるだけで、コードの動作には影響しません。無視してください。
@deprecated
タグのないまったく同じ関数である、現在エクスポートされているlegacy_createStore
APIを使用するように切り替えます。最も簡単な方法は、import { legacy_createStore as createStore } from 'redux'
のように、エイリアス付きのインポート名を変更することです。
TypeScriptの書き直し
2019年、私たちはコミュニティ主導でReduxコードベースのTypeScriptへの変換を開始しました。#3500: TypeScriptへの移植で元の取り組みが議論され、その作業はPR #3536: TypeScriptへの変換に統合されました。
しかし、既存のエコシステムとの互換性の問題(そして私たちの側の一般的な慣性)に関する懸念のために、TSに変換されたコードは数年間にわたってリポジトリ内に未使用かつ未公開のまま残っていました。
Reduxコアv5は、そのTS変換済みのソースコードから構築されています。理論的には、これはランタイム動作と型において4.xビルドとほぼ同一のはずですが、変更の一部が型の問題を引き起こす可能性が非常に高いです。
Githubで予期しない互換性の問題を報告してください!
UnknownAction
を優先してAnyAction
が非推奨化されました
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拡張機能を持つ型として指定されていました。これは、もはや適用されなくなっているものも含みます。- 技術的には、
next
を基本Reduxストアで実装されたデフォルトのDispatchとして型指定することはほぼ安全ですが、これにより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 | undefined
はpreloadedState
に渡すことができるものを正確に記述しています。しかし、combineReducers
関数はPartial
の事前読み込み状態を許可します。 | undefined
解決策は、reducerが事前読み込み状態として受け入れるものを表す個別のジェネリックを持つことです。そうすれば、createStore
はそのジェネリックをpreloadedState
引数に使用できます。
以前は、これは$CombinedState
型によって処理されていましたが、これは複雑な問題を引き起こし、ユーザーからいくつかの問題が報告されました。これにより、$CombinedState
が完全に不要になります。
この変更にはいくつかの破壊的変更が含まれていますが、全体として、ユーザーランドでアップグレードするユーザーへの影響はそれほど大きくありません。
Reducer
、ReducersMapObject
、およびcreateStore
/configureStore
の型/関数は、S
をデフォルトとする追加のPreloadedState
ジェネリックを受け入れます。combineReducers
のオーバーロードは、ReducersMapObject
をジェネリックパラメーターとして受け取る単一の関数定義に置き換えられました。オーバーロードの削除は、これらの変更に伴い、間違ったオーバーロードが選択されることがあったため必要でした。- reducerのジェネリックを明示的にリストするエンハンサーは、3番目のジェネリックを追加する必要があります。
ツールキットのみ
createSlice.extraReducers
とcreateReducer
のオブジェクト構文の削除
RTKのcreateReducer
APIは、もともとアクション型文字列のルックアップテーブルをケースreducerに受け入れるように設計されていました(例:{ "ADD_TODO": (state, action) => {} }
)。その後、「マッチャー」とデフォルトハンドラーを追加する柔軟性を高めるために「ビルダーコールバック」形式を追加し、createSlice.extraReducers
についても同様の処理を行いました。
RTK 2.0では、createReducer
とcreateSlice.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
はコールバックである必要があります
当初から、configureStore
はmiddleware
オプションとして直接配列値を受け入れていました。しかし、配列を直接提供すると、configureStore
がgetDefaultMiddleware()
を呼び出すことができなくなります。そのため、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)
},
})
スタンドアロンのgetDefaultMiddleware
とgetType
の削除
スタンドアロンバージョンのgetDefaultMiddleware
は、v1.6.1以降非推奨となっており、現在は削除されました。代わりに、ミドルウェアコールバックに渡される関数を使用してください。これは正しい型を持っています。
createAction
を使用して作成されたアクションクリエーターから型文字列を抽出するために使用されていたgetType
エクスポートも削除しました。代わりに、静的プロパティactionCreator.type
を使用してください。
RTKクエリ動作の変更
dispatch(endpoint.initiate(arg, {subscription: false}))
の使用に関するRTKクエリの問題に関する多くの報告がありました。複数のトリガーされた遅延クエリが間違ったタイミングでプロミスを解決するという報告もありました。これらにはどちらも同じ根本的な問題がありました。それは、RTKQがこれらのケースで(意図的に)キャッシュエントリを追跡していなかったことです。キャッシュエントリを常に追跡し(必要に応じて削除する)ロジックを改良しました。これにより、これらの動作の問題が解決されるはずです。
また、複数の実行を試みることと、タグの無効化がどのように動作するかについても問題が発生しました。RTKQには、複数の無効化をまとめて処理できるように、タグの無効化を短時間遅らせる内部ロジックがあります。これは、createApi
の新しいinvalidationBehavior: 'immediate' | 'delayed'
フラグによって制御されます。新しいデフォルトの動作は'delayed'
です。RTK 1.9の動作に戻すには、'immediate'
に設定します。
RTK 1.9では、RTKクエリの内部を改良し、ほとんどのサブスクリプションステータスをRTKQミドルウェア内に保持するようにしました。値は依然としてReduxストアの状態に同期されますが、これは主にRedux DevToolsの「RTK Query」パネルによる表示のためです。上記のキャッシュエントリの変更に関連して、これらの値がReduxの状態に同期される頻度をパフォーマンスのために最適化しました。
reactHooksModule
カスタムフック構成
以前は、React Reduxのフック(useSelector
、useDispatch
、useStore
)のカスタムバージョンをreactHooksModule
に個別に渡すことができ、通常はデフォルトのReactReduxContext
とは異なるコンテキストを使用できるようにしていました。
実際には、reactフックモジュールはこれらの3つのフックすべてを提供する必要があり、useStore
なしでuseSelector
とuseDispatch
のみを渡すという簡単な間違いが発生していました。
モジュールは、これらの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バイト節約されます(実際の利点は、どのインポートが使用されているかによって異なります)。
configureStore
のmiddleware
のフィールド順序が重要です
middleware
とenhancers
の両方のフィールドをconfigureStore
に渡す場合は、内部TS推論が正しく機能するためには、middleware
フィールドが最初に来る必要があります。
デフォルト以外のミドルウェア/エンハンサーはTuple
を使用する必要があります
middleware
パラメーターをconfigureStoreに渡すユーザーが、getDefaultMiddleware()
によって返される配列のスプレッドを試みたり、代替のプレーン配列を渡したりした多くのケースが見られました。残念ながら、これにより個々のミドルウェアからの正確なTS型が失われ、多くの場合、後でTSの問題が発生します(dispatch
がDispatch
として型指定され、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` でした。TypeScript は、エンティティ型の `.id` フィールド、または `selectId` の戻り値のいずれかから、正確な型を推論しようとします。ジェネリック型を直接渡すこともできます。**`EntityState` 型を直接使用する場合、両方のジェネリック引数を必ず指定する必要があります!**
`.entities` ルックアップテーブルは、標準的な TypeScript の `Record
ルックアップが `undefined` になる可能性があると想定する場合は、TypeScript の `noUncheckedIndexedAccess` 構成オプションを使用して制御できます。
Reselect
createSelector
はデフォルトのメモ化関数として `weakMapMemoize` を使用する
**createSelector
は、weakMapMemoize
という新しいデフォルトのメモ化関数を使用するようになりました。** このメモ化関数は、事実上無限のキャッシュサイズを提供するため、さまざまな引数を使用する場合に簡素化されますが、参照比較のみに依存します。
等価性の比較をカスタマイズする必要がある場合は、元の `lruMemoize` メソッドを使用するように `createSelector` をカスタマイズしてください。
createSelector(inputs, resultFn, {
memoize: lruMemoize,
memoizeOptions: { equalityCheck: yourEqualityFunction },
})
defaultMemoize
が lruMemoize
に名前変更
元の `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
型は削除されました。代わりに `Selector` と `OutputSelector` を使用してください。
React-Redux
React 18 が必要
React-Redux v7 と v8 は、フックをサポートするすべてのバージョンの React (16.8 以降、17、および 18) で動作しました。v8 は、内部のサブスクリプション管理から React の新しい `useSyncExternalStore` フックに切り替えましたが、React 16.8 と 17 (このフックが組み込まれていない) をサポートするために「シム」実装を使用していました。
**React-Redux v9 は、React 18 を _必須_ とし、React 16 または 17 を_サポートしません_。** これにより、シムを削除し、バンドルサイズを少し削減できます。
カスタムコンテキストの型付け
React Redux は、カスタムコンテキストを使用して `hooks` (および `connect`) を作成できますが、この型付けは標準化されていませんでした。v9 より前の型では `Context
import { createContext } from 'react'
import {
ReactReduxContextValue,
createDispatchHook,
createSelectorHook,
createStoreHook,
} from 'react-redux'
import { AppStore, RootState, AppDispatch } from './store'
const context = createContext<ReactReduxContextValue>(null as any)
export const useStore = createStoreHook(context).withTypes<AppStore>()
export const useDispatch = createDispatchHook(context).withTypes<AppDispatch>()
export const useSelector = createSelectorHook(context).withTypes<RootState>()
v9 では、型がランタイムの動作と一致するようになりました。コンテキストは `ReactReduxContextValue | null` を保持するように型付けされ、フックは `null` を受信した場合にエラーをスローすることを認識しているため、戻り値に影響しません。
上記の例は、次のようになります。
import { createContext } from 'react'
import {
ReactReduxContextValue,
createDispatchHook,
createSelectorHook,
createStoreHook,
} from 'react-redux'
import { AppStore, RootState, AppDispatch } from './store'
const context = createContext<ReactReduxContextValue | null>(null)
export const useStore = createStoreHook(context).withTypes<AppStore>()
export const useDispatch = createDispatchHook(context).withTypes<AppDispatch>()
export const useSelector = createSelectorHook(context).withTypes<RootState>()
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()` メソッドが添付されています。また、後で追加されるリデューサの TypeScript 型を生成するために使用できる `.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(),
)
createSlice
の `selectors` フィールド
既存の `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 つの主要な問題と、2 番目の懸念事項がありました。
- 内部で thunk を宣言するための構文が明確ではありませんでした。
- Thunk は `getState` と `dispatch` にアクセスできますが、`RootState` と `AppDispatch` の型は通常ストアから推論され、ストアはスライス状態の型から推論されます。`createSlice` の内部で thunk を宣言すると、ストアはスライスの型を必要とする一方で、スライスはストアの型を必要とするため、循環型推論エラーが発生します。JS ユーザーには問題なくても TS ユーザーには問題のある API を出荷することは望んでいませんでした。特に、RTK で TS を使用してもらいたいと考えているからです。
- ES モジュールでは同期的な条件付きインポートは行えず、`createAsyncThunk` のインポートをオプションにする良い方法がありません。`createSlice` が常にそれに依存する(そしてそれをバンドルサイズに追加する)か、`createAsyncThunk` をまったく使用できません。
これらの妥協案に落ち着きました。
createSlice
で非同期 thunk を作成するには、createAsyncThunk
にアクセスできるカスタムバージョンの `createSlice` を設定する必要があります。.- RTK Query の `createApi` の `build` コールバック構文と同様に、「クリエイターコールバック」構文を使用して `createSlice.reducers` の内部で thunk を宣言できます(型付き関数を使用してオブジェクト内のフィールドを作成します)。これは、`reducers` フィールドの既存の「オブジェクト」構文とは少し異なりますが、かなり似ています。
- `createSlice` の内部にある thunk の一部の型はカスタマイズできますが、`state` または `dispatch` の型はカスタマイズできません。それらが必要な場合は、`getState() as RootState` のように、手動で `as` キャストを行うことができます。
実際には、これらは妥当な妥協案だと考えています。`createSlice` の内部で thunk を作成することは広く求められてきたため、使用される API になると考えています。TS のカスタマイズオプションが制限になる場合は、常に `createSlice` の外部で thunk を宣言できます。そして、ほとんどの非同期 thunk は `dispatch` や `getState` を必要としません。データを取得して返すだけです。最後に、カスタム `createSlice` を設定すると、バンドルサイズに `createAsyncThunk` を含めることを選択できます(直接使用する場合や RTK Query の一部として使用する場合、すでに含まれている可能性があります。どちらの場合も、バンドルサイズは_追加_されません)。
新しいコールバック構文は次のようになります。
const createAppSlice = buildCreateSlice({
creators: { asyncThunk: asyncThunkCreator },
})
const todosSlice = createAppSlice({
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の新しい代替メモイザーのいずれか、または同等のシグネチャを持つ他のメモ化ライブラリを使用する場合に役立ちます。
Immer 10.0
Immer 10.0が正式リリースされ、いくつかの主要な改善と更新が行われました。
- 大幅に高速化された更新パフォーマンス
- 大幅に縮小されたバンドルサイズ
- 改善されたESM/CJSパッケージフォーマット
- デフォルトエクスポートなし
- ES5フォールバックなし
RTKを最終的なImmer 10.0リリースに依存するように更新しました。
Next.js設定ガイド
Next.jsでReduxを適切に設定する方法について説明するドキュメントページが追加されました。https://redux.dokyumento.jp/usage/nextjs 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を取り込むすべてのパッケージが同じバージョンを受け取るようにすることができます。
{
"overrides": {
"redux-persist": {
"redux": "^5.0.0"
}
}
}
{
"overrides": {
"redux": "^5.0.0"
}
}
`yarn` - `resolutions`
Yarnは、`package.json``resolutions`フィールドを通じてこれをサポートしています。NPMと同様に、特定のパッケージの依存関係をオーバーライドするか、Reduxを取り込むすべてのパッケージが同じバージョンを受け取るようにすることができます。
{
"resolutions": {
"redux-persist/redux": "^5.0.0"
}
}
{
"resolutions": {
"redux": "^5.0.0"
}
}
推奨事項
2.0および以前のバージョンの変更に基づいて、重要ではないにしても知っておくと良い考え方の変化がいくつかあります。
`actionCreator.toString()`の代替手段
`createAction`で作成されたアクションクリエーターは、RTKの元のAPIの一部として、アクション型を返すカスタム`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 1、PR 2)が、何も決定されていません。これをサポートしてほしい場合は、Githubのディスカッションにご意見をお寄せください!
3.0 - RTK Query
RTK 2.0は主にコアとツールキットの変更に焦点を当てていました。2.0がリリースされたので、まだ解決すべき粗い部分があるRTK Queryに焦点を移したいと考えています。その中には破壊的変更が必要なものもあり、3.0リリースが必要になる可能性があります。
それについてご意見がありましたら、RTK Query APIの課題と粗い部分に関するフィードバックスレッドにご意見をお寄せください!