Skip to content

Commit 478ffcf

Browse files
authored
Merge pull request #2138 from reduxjs/docs/listener-docs-types
2 parents 03eafd5 + f364917 commit 478ffcf

File tree

1 file changed

+142
-5
lines changed

1 file changed

+142
-5
lines changed

docs/api/createListenerMiddleware.mdx

Lines changed: 142 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,76 @@ store.dispatch(clearAllListeners())
300300

301301
## Listener API
302302

303-
The `listenerApi` object is the second argument to each listener callback. It contains several utility functions that may be called anywhere inside the listener's logic. These can be divided into several categories.
303+
The `listenerApi` object is the second argument to each listener callback. It contains several utility functions that may be called anywhere inside the listener's logic.
304+
305+
```ts no-transpile
306+
export interface ListenerEffectAPI<
307+
State,
308+
Dispatch extends ReduxDispatch<AnyAction>,
309+
ExtraArgument = unknown
310+
> extends MiddlewareAPI<Dispatch, State> {
311+
// NOTE: MiddlewareAPI contains `dispatch` and `getState` already
312+
313+
/**
314+
* Returns the store state as it existed when the action was originally dispatched, _before_ the reducers ran.
315+
* This function can **only** be invoked **synchronously**, it throws error otherwise.
316+
*/
317+
getOriginalState: () => State
318+
/**
319+
* Removes the listener entry from the middleware and prevent future instances of the listener from running.
320+
* It does **not** cancel any active instances.
321+
*/
322+
unsubscribe(): void
323+
/**
324+
* It will subscribe a listener if it was previously removed, noop otherwise.
325+
*/
326+
subscribe(): void
327+
/**
328+
* Returns a promise that resolves when the input predicate returns `true` or
329+
* rejects if the listener has been cancelled or is completed.
330+
*
331+
* The return value is `true` if the predicate succeeds or `false` if a timeout is provided and expires first.
332+
*/
333+
condition: ConditionFunction<State>
334+
/**
335+
* Returns a promise that resolves when the input predicate returns `true` or
336+
* rejects if the listener has been cancelled or is completed.
337+
*
338+
* The return value is the `[action, currentState, previousState]` combination that the predicate saw as arguments.
339+
*
340+
* The promise resolves to null if a timeout is provided and expires first.
341+
*/
342+
take: TakePattern<State>
343+
/**
344+
* Cancels all other running instances of this same listener except for the one that made this call.
345+
*/
346+
cancelActiveListeners: () => void
347+
/**
348+
* An abort signal whose `aborted` property is set to `true`
349+
* if the listener execution is either aborted or completed.
350+
* @see https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
351+
*/
352+
signal: AbortSignal
353+
/**
354+
* Returns a promise resolves after `timeoutMs` or
355+
* rejects if the listener has been cancelled or is completed.
356+
*/
357+
delay(timeoutMs: number): Promise<void>
358+
/**
359+
* Queues in the next microtask the execution of a task.
360+
*/
361+
fork<T>(executor: ForkedTaskExecutor<T>): ForkedTask<T>
362+
/**
363+
* Returns a promise that resolves when `waitFor` resolves or
364+
* rejects if the listener has been cancelled or is completed.
365+
* @param promise
366+
*/
367+
pause<M>(promise: Promise<M>): Promise<M>
368+
extra: ExtraArgument
369+
}
370+
```
371+
372+
These can be divided into several categories.
304373

305374
### Store Interaction Methods
306375

@@ -395,9 +464,15 @@ import type { RootState, AppDispatch } from './store'
395464

396465
export const listenerMiddleware = createListenerMiddleware()
397466

467+
export type AppStartListening = TypedStartListening<RootState, AppDispatch>
468+
398469
export const startAppListening =
399-
listenerMiddleware.startListening as TypedStartListening<RootState, AppDispatch>
400-
export const addAppListener = addListener as TypedAddListener<RootState, AppDispatch>
470+
listenerMiddleware.startListening as AppStartListening
471+
472+
export const addAppListener = addListener as TypedAddListener<
473+
RootState,
474+
AppDispatch
475+
>
401476
```
402477

403478
Then import and use those pre-typed methods in your components.
@@ -410,7 +485,7 @@ This middleware lets you run additional logic when some action is dispatched, as
410485

411486
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!).
412487

413-
The middleware includes several async workflow primitives that are sufficient to write equivalents to many Redux-Saga effects operators like `takeLatest`, `takeLeading`, and `debounce`.
488+
The middleware includes several async workflow primitives that are sufficient to write equivalents to many Redux-Saga effects operators like `takeLatest`, `takeLeading`, and `debounce`, although none of those methods are directly included. (See [the listener middleware tests file for examples of how to write code equivalent to those effects](https://github.com/reduxjs/redux-toolkit/blob/03eafd5236f16574935cdf1c5958e32ee8cf3fbe/packages/toolkit/src/listenerMiddleware/tests/effectScenarios.test.ts#L74-L363).)
414489

415490
### Standard Usage Patterns
416491

@@ -573,7 +648,7 @@ listenerMiddleware.startListening({
573648
574649
### Complex Async Workflows
575650
576-
The provided async workflow primitives (`cancelActiveListeners`, `unsuscribe`, `subscribe`, `take`, `condition`, `pause`, `delay`) can be used to implement many of the more complex async workflow capabilities found in the Redux-Saga library. This includes effects such as `throttle`, `debounce`, `takeLatest`, `takeLeading`, and `fork/join`. Some examples from the test suite:
651+
The provided async workflow primitives (`cancelActiveListeners`, `unsuscribe`, `subscribe`, `take`, `condition`, `pause`, `delay`) can be used to implement behavior that is equivalent to many of the more complex async workflow capabilities found in the Redux-Saga library. This includes effects such as `throttle`, `debounce`, `takeLatest`, `takeLeading`, and `fork/join`. Some examples from the test suite:
577652
578653
```js
579654
test('debounce / takeLatest', async () => {
@@ -729,3 +804,65 @@ While this pattern is _possible_, **we do not necessarily _recommend_ doing this
729804
At the same time, this _is_ a valid technique, both in terms of API behavior and potential use cases. It's been common to lazy-load sagas as part of a code-split app, and that has often required some complex additional setup work to "inject" sagas. In contrast, `dispatch(addListener())` fits naturally into a React component's lifecycle.
730805
731806
So, while we're not specifically encouraging use of this pattern, it's worth documenting here so that users are aware of it as a possibility.
807+
808+
### Organizing Listeners in Files
809+
810+
As a starting point, **it's best to create the listener middleware in a separate file, such as `app/listenerMiddleware.ts`, rather than in the same file as the store**. This avoids any potential circular import problems from other files trying to import `middleware.addListener`.
811+
812+
From there, so far we've come up with three different ways to organize listener functions and setup.
813+
814+
First, you can import effect callbacks from slice files into the middleware file, and add the listeners:
815+
816+
```ts no-transpile title="app/listenerMiddleware.ts"
817+
import { action1, listener1 } from '../features/feature1/feature1Slice'
818+
import { action2, listener2 } from '../features/feature2/feature1Slice'
819+
820+
listenerMiddleware.startListening({ actionCreator: action1, effect: listener1 })
821+
listenerMiddleware.startListening({ actionCreator: action2, effect: listener2 })
822+
```
823+
824+
This is probably the simplest option, and mirrors how the store setup pulls together all the slice reducers to create the app.
825+
826+
The second option is the opposite: have the slice files import the middleware and directly add their listeners:
827+
828+
```ts no-transpile title="features/feature1/feature1Slice.ts"
829+
import { listenerMiddleware } from '../../app/listenerMiddleware'
830+
831+
const feature1Slice = createSlice(/* */)
832+
const { action1 } = feature1Slice.actions
833+
834+
export default feature1Slice.reducer
835+
836+
listenerMiddleware.startListening({
837+
actionCreator: action1,
838+
effect: () => {},
839+
})
840+
```
841+
842+
This keeps all the logic in the slice, although it does lock the setup into a single middleware instance.
843+
844+
The third option is to create a setup function in the slice, but let the listener file call that on startup:
845+
846+
```ts no-transpile title="features/feature1/feature1Slice.ts"
847+
import type { AppStartListening } from '../../app/listenerMiddleware'
848+
849+
const feature1Slice = createSlice(/* */)
850+
const { action1 } = feature1Slice.actions
851+
852+
export default feature1Slice.reducer
853+
854+
export const addFeature1Listeners = (startListening: AppStartListening) => {
855+
startListening({
856+
actionCreator: action1,
857+
effect: () => {},
858+
})
859+
}
860+
```
861+
862+
```ts no-transpile title="app/listenerMiddleware.ts"
863+
import { addFeature1Listeners } from '../features/feature1/feature1Slice'
864+
865+
addFeature1Listeners(listenerMiddleware.startListening)
866+
```
867+
868+
Feel free to use whichever of these approaches works best in your app.

0 commit comments

Comments
 (0)