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

カスタムミドルウェアの作成

学習内容
  • カスタムミドルウェアを使用する場合
  • ミドルウェアの標準的なパターン
  • ミドルウェアが他の Redux プロジェクトと互換性があることを確認する方法

Redux のミドルウェアは主に次のいずれかに使用できます。

  • アクションの副作用を作成する。
  • アクションを変更またはキャンセルする。
  • または、dispatch によって受け入れられる入力を変更する。

ほとんどのユースケースは最初のカテゴリに分類されます。たとえば、Redux-Sagaredux-observable、およびRTKリスナーミドルウェアはすべて、アクションに反応する副作用を作成します。これらの例は、これが非常に一般的なニーズであることも示しています。つまり、状態の変更以外の方法でアクションに反応できるようにする必要があります。

アクションの変更は、例えば、状態または外部入力からの情報でアクションを拡張したり、それらをスロットル、デバウンス、またはゲートしたりするために使用できます。

dispatch の入力を変更する最も明白な例はRedux Thunkです。これは、アクションを返す関数を呼び出すことによってアクションに変換します。

カスタムミドルウェアを使用する場合

ほとんどの場合、実際にはカスタムミドルウェアは必要ありません。ミドルウェアの最も可能性の高いユースケースは副作用であり、Redux の副作用をうまくパッケージ化し、自分でこれを構築するときに遭遇する微妙な問題を解消するのに十分な期間使用されているパッケージが多数あります。サーバー側の状態を管理するためのRTK Queryと、その他の副作用のためのRTKリスナーミドルウェアが良い出発点です。

次のいずれかのケースでは、カスタムミドルウェアを使用することがあります。

  1. 単一の非常に単純な副作用しかない場合は、完全な追加のフレームワークを追加する価値がない可能性があります。独自のカスタムソリューションを拡張するのではなく、アプリケーションが成長したら既存のフレームワークに切り替えるようにしてください。
  2. アクションを変更またはキャンセルする必要がある場合。

ミドルウェアの標準的なパターン

アクションの副作用を作成する

これが最も一般的なミドルウェアです。これは、rtkリスナーミドルウェアの場合の例です。

const middleware: ListenerMiddleware<S, D, ExtraArgument> =
api => next => action => {
if (addListener.match(action)) {
return startListening(action.payload)
}

if (clearAllListeners.match(action)) {
clearListenerMiddleware()
return
}

if (removeListener.match(action)) {
return stopListening(action.payload)
}

// Need to get this state _before_ the reducer processes the action
let originalState: S | typeof INTERNAL_NIL_TOKEN = api.getState()

// `getOriginalState` can only be called synchronously.
// @see https://github.com/reduxjs/redux-toolkit/discussions/1648#discussioncomment-1932820
const getOriginalState = (): S => {
if (originalState === INTERNAL_NIL_TOKEN) {
throw new Error(
`${alm}: getOriginalState can only be called synchronously`
)
}

return originalState as S
}

let result: unknown

try {
// Actually forward the action to the reducer before we handle listeners
result = next(action)

if (listenerMap.size > 0) {
let currentState = api.getState()
// Work around ESBuild+TS transpilation issue
const listenerEntries = Array.from(listenerMap.values())
for (let entry of listenerEntries) {
let runListener = false

try {
runListener = entry.predicate(action, currentState, originalState)
} catch (predicateError) {
runListener = false

safelyNotifyError(onError, predicateError, {
raisedBy: 'predicate'
})
}

if (!runListener) {
continue
}

notifyListener(entry, action, api, getOriginalState)
}
}
} finally {
// Remove `originalState` store from this scope.
originalState = INTERNAL_NIL_TOKEN
}

return result
}

最初の部分では、後で呼び出すリスナーを変更するために、addListenerclearAllListeners、およびremoveListenerアクションをリッスンします。

2番目の部分では、コードは主に、他のミドルウェアとリデューサーを介してアクションを渡した後の状態を計算し、リデューサーから得られた元の状態と新しい状態の両方をリスナーに渡します。

アクションのディスパッチ後に副作用が発生するのが一般的です。これは、元の状態と新しい状態の両方を考慮に入れることができ、副作用からのインタラクションは現在の action 実行に影響を与えない必要があるためです(そうでない場合、それは副作用ではありません)。

アクションの変更またはキャンセル、または dispatch で受け入れられる入力の変更

これらのパターンは一般的ではありませんが、ほとんど(アクションのキャンセルを除く)はredux thunk middlewareで使用されています。

const middleware: ThunkMiddleware<State, BasicAction, ExtraThunkArg> =
({ dispatch, getState }) =>
next =>
action => {
// The thunk middleware looks for any functions that were passed to `store.dispatch`.
// If this "action" is really a function, call it and return the result.
if (typeof action === 'function') {
// Inject the store's `dispatch` and `getState` methods, as well as any "extra arg"
return action(dispatch, getState, extraArgument)
}

// Otherwise, pass the action down the middleware chain as usual
return next(action)
}

通常、dispatchは JSON アクションのみを処理できます。このミドルウェアは、関数の形式のアクションも処理する機能を追加します。また、関数アクションの戻り値を dispatch 関数の戻り値として渡すことにより、dispatch 関数自体の戻り値の型も変更します。

互換性のあるミドルウェアを作成するためのルール

原則として、ミドルウェアは非常に強力なパターンであり、アクションに対して何でも実行できます。ただし、既存のミドルウェアは、その周りのミドルウェアで何が起こるかについて前提を持っている可能性があり、これらの前提を認識することで、ミドルウェアが既存の一般的なミドルウェアとうまく連携することが容易になります。

ミドルウェアと他のミドルウェアの間には、2つの接点があります。

次のミドルウェアを呼び出す

nextを呼び出すと、ミドルウェアはアクションの形式を期待します。明示的に変更したい場合を除き、受け取ったアクションをそのまま渡してください。

より微妙な点として、一部のミドルウェアは、dispatchが呼び出されるのと同じティックでミドルウェアが呼び出されることを期待しているため、nextはミドルウェアによって同期的に呼び出す必要があります。

dispatch の戻り値を返す

ミドルウェアが dispatch の戻り値を明示的に変更する必要がない限り、next から取得したものを返すだけです。戻り値を変更する必要がある場合は、ミドルウェアが本来の機能を実行できるように、ミドルウェアチェーン内の非常に特定の場所に配置する必要があります。他のすべてのミドルウェアとの互換性を手動で確認し、それらを連携させる方法を決定する必要があります。

これには、厄介な結果があります。

const middleware: Middleware = api => next => async action => {
const response = next(action)

// Do something after the action hits the reducer
const afterState = api.getState()
if (action.type === 'some/action') {
const data = await fetchData()
api.dispatch(dataFetchedAction(data))
}

return response
}

レスポンスを変更していないように見えても、実際には変更しました。async-await により、これは現在Promiseです。これにより、RTK Query のミドルウェアなど、一部のミドルウェアが壊れます。

では、代わりにこのミドルウェアをどのように記述できるでしょうか?

const middleware: Middleware = api => next => action => {
const response = next(action)

// Do something after the action hits the reducer
const afterState = api.getState()
if (action.type === 'some/action') {
void loadData(api)
}

return response
}

async function loadData(api) {
const data = await fetchData()
api.dispatch(dataFetchedAction(data))
}

asyncロジックを別の関数に移動するだけで、async-await を使用できますが、ミドルウェアでPromiseが解決されるのを実際に待つことはありません。voidは、コードを読んでいる他の人に、コードに影響を与えることなく、明示的に Promise を await しないことを決定したことを示します。

次のステップ

まだの場合は、Redux の理解のミドルウェアセクションを参照して、ミドルウェアがどのように機能するかを理解してください。