You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
// Run whatever additional side-effect-y logic you want here
33
34
const { text } =action.payload
34
35
console.log('Todo added: ', text)
35
36
36
37
if (text ==='Buy milk') {
37
38
// Use the listener API methods to dispatch, get state, or unsubscribe the listener
39
+
listenerApi.dispatch(todoAdded('Buy pet food'))
38
40
listenerApi.unsubscribe()
39
41
}
40
42
})
@@ -84,34 +86,88 @@ For more background and debate over the use cases and API design, see the origin
84
86
-[RTK issue #237: Add an action listener middleware](https://github.com/reduxjs/redux-toolkit/issues/237)
85
87
-[RTK PR #547: yet another attempt at an action listener middleware](https://github.com/reduxjs/redux-toolkit/pull/547)
86
88
87
-
## Usage and API
89
+
## API Reference
88
90
89
91
`createActionListenerMiddleware` lets you add listeners by providing an action type and a callback, lets you specify whether your callback should run before or after the action is processed by the reducers, and gives you access to `dispatch` and `getState` for use in your logic. Callbacks can also unsubscribe.
90
92
91
93
Listeners can be defined statically by calling `listenerMiddleware.addListener()` during setup, or added and removed dynamically at runtime with special `dispatch(addListenerAction())` and `dispatch(removeListenerAction())` actions.
-`extra`: an optional "extra argument" that will be injected into the `listenerApi` parameter of each listener. Equivalent to [the "extra argument" in the Redux Thunk middleware](https://redux.js.org/usage/writing-logic-thunks#injecting-config-values-into-thunks).
Statically adds a new listener callback to the middleware.
100
106
101
107
Parameters:
102
108
103
-
-`actionType: string | ActionCreator | Matcher`: Determines which action(s) will cause the `listener` callback to run. May be a plain action type string, a standard RTK-generated action creator with a `.type` field, or an RTK "matcher" function. The listener will be run if the current action's `action.type` string is an exact match, or if the matcher function returns true.
104
-
-`listener: (action: Action, listenerApi: ListenerApi) => void`: the listener callback. Will receive the current action as its first argument. The second argument is a "listener API" object similar to the "thunk API" object in `createAsyncThunk`. It contains the usual `dispatch` and `getState` store methods, as well as two listener-specific methods: `unsubscribe` will remove the listener from the middleware, and `stopPropagation` will prevent any further listeners from handling this specific action.
109
+
-`predicate: string | ActionCreator | Matcher | ListenerPredicate`: Determines which action(s) will cause the `listener` callback to run. May be a plain action type string, a standard RTK-generated action creator with a `.type` field, an RTK "matcher" function, or a "listener predicate" that also receives the current and original state. The listener will be run if the current action's `action.type` string is an exact match, or if the matcher/predicate function returns `true`.
110
+
-`listener: (action: Action, listenerApi: ListenerApi) => void`: the listener callback. Will receive the current action as its first argument, as well as a "listener API" object similar to the "thunk API" object in `createAsyncThunk`. It contains:
111
+
-`dispatch: Dispatch`: the standard `store.dispatch` method
112
+
-`getState: () => State`: the standard `store.getState` method
113
+
-`getOriginalState: () => State`: returns the store state as it existed when the action was originally dispatched, _before_ the reducers ran
114
+
-`currentPhase: 'beforeReducer' | 'afterReducer'`: an string indicating when the listener is being called relative to the action processing
115
+
-`condition: (predicate: ListenerPredicate, timeout?) => Promise<boolean>`: allows async logic to pause and wait for some condition to occur before continuing. See "Writing Async Workflows" below for details on usage.
116
+
-`extra`: the "extra argument" that was provided as part of the middleware setup, if any
117
+
-`unsubscribe` will remove the listener from the middleware
105
118
-`options: {when?: 'before' | 'after'}`: an options object. Currently only one options field is accepted - an enum indicating whether to run this listener 'before' the action is processed by the reducers, or 'after'. If not provided, the default is 'after'.
106
119
107
-
The return value is a standard `unsubscribe()` callback that will remove this listener.
120
+
The return value is a standard `unsubscribe()` callback that will remove this listener. If a listener entry with this exact function reference already exists, no new entry will be added, and the existing `unsubscribe` method will be returned.
121
+
122
+
Adding a listener takes a "listener predicate" callback, which will be called when an action is dispatched, and should return `true` if the listener itself should be called:
The listener may be configured to run _before_ an action reaches the reducer, _after_ the reducer, or both, by passing a `{when}` option when adding the listener:
Removes a given listener based on an action type string and a listener function reference.
108
158
109
159
### `addListenerAction`
110
160
111
-
A standard RTK action creator that tells the middleware to add a new listener at runtime. It accepts the same arguments as `listenerMiddleware.addListener()`.
161
+
A standard RTK action creator that tells the middleware to dynamcially add a new listener at runtime.
162
+
163
+
> **NOTE**: It is intended to eventually accept the same arguments as `listenerMiddleware.addListener()`, but currently only accepts action types and action creators - this will hopefully be fixed in a later update.
112
164
113
165
Dispatching this action returns an `unsubscribe()` callback from `dispatch`.
A standard RTK action creator that tells the middleware to remove a listener at runtime. It requires two arguments:
@@ -120,3 +176,121 @@ A standard RTK action creator that tells the middleware to remove a listener at
120
176
-`listener: ListenerCallback`: the same listener callback reference that was added originally
121
177
122
178
Note that matcher-based listeners currently cannot be removed with this approach - you must use the `unsubscribe()` callback that was returned when adding the listener.
179
+
180
+
## Usage Guide
181
+
182
+
### Overall Purpose
183
+
184
+
This middleware lets you run additional logic when some action is dispatched, as a lighter-weight alternative to middleware like sagas and observables that have both a heavy runtime bundle cost and a large conceptual overhead.
185
+
186
+
This middleware is not intended to handle all possible use cases. Like thunks, it provides you with a basic set of primitives (including access to `dispatch` and `getState`), and gives you freedom to write any sync or async logic you want. This is both a strength (you can do anything!) and a weakness (you can do anything, with no guard rails!).
187
+
188
+
### Standard Usage Patterns
189
+
190
+
The most common expected usage is "run some logic after a given action was dispatched". For example, you could set up a simple analytics tracker by looking for certain actions and sending extracted data to the server, including pulling user details from the store:
You could also implement a generic API fetching capability, where the UI dispatches a plain action describing the type of resource to be requested, and the middleware automatically fetches it and dispatches a result action:
The provided `listenerPredicate` should be `(action, currentState?, originalState?) => boolean`
218
+
219
+
The `listenerApi.unsubscribe` method may be used at any time, and will remove the listener from handling any future actions. As an example, you could create a one-shot listener by unconditionally calling `unsubscribe()` in the body - it would run the first time the relevant action is seen, and then immediately stop and not handle any future actions.
220
+
221
+
### Writing Async Workflows
222
+
223
+
One of the great strengths of both sagas and observables is their support for complex async workflows, including stopping and starting behavior based on specific dispatched actions. However, the weakness is that both require mastering a complex API with many unique operators (effects methods like `call()` and `fork()` for sagas, RxJS operators for observables), and both add a significant amount to application bundle size.
224
+
225
+
While this middleware is _not_ at all meant to fully replace those, it has some ability to implement long-running async workflows as well, using the `condition` method in `listenerApi`. This method is directly inspired by [the `condition` function in Temporal.io's workflow API](https://docs.temporal.io/docs/typescript/workflows/#condition) (credit to [@swyx](https://twitter.com/swyx) for the suggestion!).
You can use `awaitcondition(somePredicate)` as a way to pause execution of your listener callback until some criteria is met.
237
+
238
+
The `predicate` will be called before and after every action is processed, and should return `true` when the condition should resolve. (It is effectively a one-shot listener itself.) If a `timeout` number (in ms) is provided, the promise will resolve `true` if the `predicate` returns first, or `false` if the timeout expires. This allows you to write comparisons like `if (awaitcondition(predicate))`.
239
+
240
+
This should enable writing longer-running workflows with more complex async logic, such as [the "cancellable counter" example from Redux-Saga](https://github.com/redux-saga/redux-saga/blob/1ecb1bed867eeafc69757df8acf1024b438a79e0/examples/cancellable-counter/src/sagas/index.js).
241
+
242
+
An example of usage, from the test suite:
243
+
244
+
```ts
245
+
test('condition method resolves promise when there is a timeout', async () => {
246
+
let finalCount =0
247
+
let listenerStarted =false
248
+
249
+
middleware.addListener(
250
+
// @ts-expect-error state declaration not yet working right
// If we wait 150ms, the condition timeout will expire first
281
+
await delay(150)
282
+
// Update the state one more time to confirm the listener isn't checking it
283
+
store.dispatch(increment())
284
+
285
+
// Handled the state update before the delay, but not after
286
+
expect(finalCount).toBe(2)
287
+
})
288
+
```
289
+
290
+
### TypeScript Usage
291
+
292
+
The code is typed, but the behavior is incomplete. In particular, the various `state`, `dispatch`, and `extra` types in the listeners are not at all connected to the actual store types. You will likely need to manually declare or cast them as appropriate for your store setup, and possibly even use `// @ts-ignore` if the compiler doesn't accept declaring those types.
293
+
294
+
## Feedback
295
+
296
+
Pleaseprovidefeedbackin [RTKdiscussion #1648: "New experimental "actionlistenermiddleware" package"](https://github.com/reduxjs/redux-toolkit/discussions/1648).
0 commit comments