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
Copy file name to clipboardExpand all lines: docs/api/getDefaultMiddleware.mdx
+1-1Lines changed: 1 addition & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -62,7 +62,7 @@ const store = configureStore({
62
62
// Store has all of the default middleware added, _plus_ the logger middleware
63
63
```
64
64
65
-
It is preferrable to use the chainable `.concat(...)` and `.prepend(...)` methods of the returned `MiddlewareArray` instead of the array spread operator, as the latter can lose valuable type information under some circumstances.
65
+
It is preferable to use the chainable `.concat(...)` and `.prepend(...)` methods of the returned `MiddlewareArray` instead of the array spread operator, as the latter can lose valuable type information under some circumstances.
Copy file name to clipboardExpand all lines: docs/rtk-query/usage/automated-refetching.mdx
+1-1Lines changed: 1 addition & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -40,7 +40,7 @@ The `providesTags` argument can either be an array of `string` (such as `['Post'
40
40
41
41
### Invalidating tags
42
42
43
-
_see also: [invalidatesTags API reference](../api/createApi.mdx#invalidatesTags)_
43
+
_see also: [invalidatesTags API reference](../api/createApi.mdx#invalidatestags)_
44
44
45
45
A _mutation_ can _invalidate_ specific cached data based on the tags. Doing so determines which cached data will be either refetched or removed from the cache.
// 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,94 @@ 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
-
`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.
91
+
`createActionListenerMiddleware` lets you add listeners by providing a "listener callback" containing additional logic, a way to specify when that callback should run based on dispatched actions or state changes, and whether your callback should run before or after the action is processed by the reducers.
92
+
93
+
The middleware then gives you access to `dispatch` and `getState` for use in your listener callback's logic. Callbacks can also unsubscribe to stop from being run again in the future.
90
94
91
95
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).
104
+
105
+
-`onError`: an optional error handler that gets called with synchronous errors raised by `listener` and `predicate`.
Statically adds a new listener callback to the middleware.
100
110
101
111
Parameters:
102
112
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.
113
+
-`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`.
114
+
-`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:
115
+
-`dispatch: Dispatch`: the standard `store.dispatch` method
116
+
-`getState: () => State`: the standard `store.getState` method
117
+
-`getOriginalState: () => State`: returns the store state as it existed when the action was originally dispatched, _before_ the reducers ran
118
+
-`currentPhase: 'beforeReducer' | 'afterReducer'`: an string indicating when the listener is being called relative to the action processing
119
+
-`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.
120
+
-`extra`: the "extra argument" that was provided as part of the middleware setup, if any
121
+
-`unsubscribe` will remove the listener from the middleware
105
122
-`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
123
107
124
The return value is a standard `unsubscribe()` callback that will remove this listener.
108
125
126
+
If you try to add a listener entry but another entry with this exact function reference already exists, no new entry will be added, and the existing `unsubscribe` method will be returned.
127
+
128
+
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.
164
+
109
165
### `addListenerAction`
110
166
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()`.
167
+
A standard RTK action creator that tells the middleware to dynamcially add a new listener at runtime.
168
+
169
+
> **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
170
113
171
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 +182,121 @@ A standard RTK action creator that tells the middleware to remove a listener at
120
182
-`listener: ListenerCallback`: the same listener callback reference that was added originally
121
183
122
184
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.
185
+
186
+
## Usage Guide
187
+
188
+
### Overall Purpose
189
+
190
+
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.
191
+
192
+
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!).
193
+
194
+
### Standard Usage Patterns
195
+
196
+
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`
224
+
225
+
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.
226
+
227
+
### Writing Async Workflows
228
+
229
+
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.
230
+
231
+
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.
243
+
244
+
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))`.
245
+
246
+
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).
247
+
248
+
An example of usage, from the test suite:
249
+
250
+
```ts
251
+
test('condition method resolves promise when there is a timeout', async () => {
252
+
let finalCount =0
253
+
let listenerStarted =false
254
+
255
+
middleware.addListener(
256
+
// @ts-expect-error state declaration not yet working right
// If we wait 150ms, the condition timeout will expire first
287
+
await delay(150)
288
+
// Update the state one more time to confirm the listener isn't checking it
289
+
store.dispatch(increment())
290
+
291
+
// Handled the state update before the delay, but not after
292
+
expect(finalCount).toBe(2)
293
+
})
294
+
```
295
+
296
+
### TypeScript Usage
297
+
298
+
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.
299
+
300
+
## Feedback
301
+
302
+
Pleaseprovidefeedbackin [RTKdiscussion #1648: "New experimental "actionlistenermiddleware" package"](https://github.com/reduxjs/redux-toolkit/discussions/1648).
0 commit comments