Skip to content

Commit cecda16

Browse files
authored
Merge pull request #1807 from reduxjs/feature/middleware-job-tests
2 parents 952a632 + 3942f8f commit cecda16

File tree

4 files changed

+719
-19
lines changed

4 files changed

+719
-19
lines changed

packages/action-listener-middleware/README.md

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ The Redux community has settled around three primary side effects libraries over
5959

6060
- Thunks use basic functions passed to `dispatch`. They let users run arbitrary logic, including dispatching actions and getting state. These are mostly used for basic AJAX requests and logic that needs to read from state before dispatching actions
6161
- Sagas use generator functions and a custom set of "effects" APIs, which are then executed by a middleware. Sagas let users write powerful async logic and workflows that can respond to any dispatched action, including "background thread"-type behavior like infinite loops and cancelation.
62-
- Observables use RxJS obesrvable operators. Observables form pipelines that do arbitrary processing similar to sagas, but with a more functional API style.
62+
- Observables use RxJS observable operators. Observables form pipelines that do arbitrary processing similar to sagas, but with a more functional API style.
6363

6464
All three of those have strengths and weaknesses:
6565

@@ -167,7 +167,7 @@ The return value is a standard `unsubscribe()` callback that will remove this li
167167

168168
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`.
169169

170-
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 listene. If the `when` option is not provided, the default is 'afterReducer':
170+
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. If the `when` option is not provided, the default is 'afterReducer':
171171

172172
```ts
173173
middleware.addListener({
@@ -228,7 +228,7 @@ The `listenerApi` object is the second argument to each listener callback. It co
228228

229229
- `unsubscribe: () => void`: will remove the listener from the middleware
230230
- `subscribe: () => void`: will re-subscribe the listener if it was previously removed, or no-op if currently subscribed
231-
- `cancelPrevious: () => void`: cancels any previously running instances of this same listener. (The cancelation will only have a meaningful effect if the previous instances are paused using one of the `job` APIs, `take`, or `condition` - see "Job Management" in the "Usage" section for more details)
231+
- `cancelPrevious: () => void`: cancels any previously running instances of this same listener. (The cancelation will only have a meaningful effect if the previous instances are paused using one of the `job` APIs, `take`, or `condition` - see "Cancelation and Job Management" in the "Usage" section for more details)
232232

233233
Dynamically unsubscribing and re-subscribing this listener allows for more complex async workflows, such as avoiding duplicate running instances by calling `listenerApi.unsubscribe()` at the start of a listener, or calling `listenerApi.cancelPrevious()` to ensure that only the most recent instance is allowed to complete.
234234

@@ -289,39 +289,40 @@ This middleware lets you run additional logic when some action is dispatched, as
289289

290290
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!).
291291

292-
As of v0.4.0, the middleware does include several async workflow primitives that are sufficient to write equivalents to many Redux-Saga effects operators, like `takeLatest`, `takeLeading`, and `debounce`.
292+
As of v0.4.0, the middleware does include several async workflow primitives that are sufficient to write equivalents to many Redux-Saga effects operators like `takeLatest`, `takeLeading`, and `debounce`.
293293

294294
### Standard Usage Patterns
295295

296296
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:
297297

298298
```js
299-
middleware.addListener(
300-
isAnyOf(action1, action2, action3),
301-
(action, listenerApi) => {
299+
middleware.addListener({
300+
matcher: isAnyOf(action1, action2, action3),
301+
listener: (action, listenerApi) => {
302302
const user = selectUserDetails(listenerApi.getState())
303303

304304
const { specialData } = action.meta
305305

306306
analyticsApi.trackUsage(action.type, user, specialData)
307-
}
308-
)
307+
},
308+
})
309309
```
310310

311311
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:
312312

313313
```js
314-
middleware.addListener(resourceRequested, async (action, listenerApi) => {
315-
const { name, args } = action.payload
316-
dispatch(resourceLoading())
314+
middleware.addListener({
315+
actionCreator: resourceRequested,
316+
listener: async (action, listenerApi) => {
317+
const { name, args } = action.payload
318+
listenerApi.dispatch(resourceLoading())
317319

318-
const res = await serverApi.fetch(`/api/${name}`, ...args)
319-
dispatch(resourceLoaded(res.data))
320+
const res = await serverApi.fetch(`/api/${name}`, ...args)
321+
listenerApi.dispatch(resourceLoaded(res.data))
322+
},
320323
})
321324
```
322325

323-
The provided `listenerPredicate` should be `(action, currentState?, originalState?) => boolean`
324-
325326
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.
326327

327328
### Writing Async Workflows with Conditions
@@ -400,7 +401,7 @@ test('condition method resolves promise when there is a timeout', async () => {
400401
})
401402
```
402403
403-
### Cancelation, and Job Management
404+
### Cancelation and Job Management
404405
405406
As of 0.4.0, the middleware now uses a `Job` abstraction to help manage cancelation of existing listener instances. The `Job` implementation is based on https://github.com/ethossoftworks/job-ts .
406407

packages/action-listener-middleware/src/job.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -248,9 +248,9 @@ export class Job<T> implements JobHandle {
248248
*/
249249
async pause<R>(func: Promise<R>): Promise<R> {
250250
this.ensureActive()
251-
const result = await func
251+
const result = await Promise.race([func, this._cancelPromise])
252252
this.ensureActive()
253-
return result
253+
return result as R
254254
}
255255

256256
/**

0 commit comments

Comments
 (0)