Skip to content

Commit ed200a3

Browse files
committed
Add onCancel callback.
1 parent f607939 commit ed200a3

File tree

4 files changed

+70
-23
lines changed

4 files changed

+70
-23
lines changed

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ error states, without assumptions about the shape of your data or the type of re
4141
- Provides convenient `isLoading`, `startedAt`, `finishedAt`, et al metadata
4242
- Provides `cancel` and `reload` actions
4343
- Automatic re-run using `watch` or `watchFn` prop
44-
- Accepts `onResolve` and `onReject` callbacks
44+
- Accepts `onResolve`, `onReject` and `onCancel` callbacks
4545
- Supports [abortable fetch] by providing an AbortController
4646
- Supports optimistic updates using `setData`
4747
- Supports server-side rendering through `initialValue`
@@ -335,6 +335,7 @@ These can be passed in an object to `useAsync()`, or as props to `<Async>` and c
335335
- `initialValue` Provide initial data or error for server-side rendering.
336336
- `onResolve` Callback invoked when Promise resolves.
337337
- `onReject` Callback invoked when Promise rejects.
338+
- `onCancel` Callback invoked when a Promise is cancelled.
338339
- `reducer` State reducer to control internal state updates.
339340
- `dispatcher` Action dispatcher to control internal action dispatching.
340341
- `debugLabel` Unique label used in DevTools.
@@ -411,6 +412,13 @@ Callback function invoked when a promise resolves, receives data as argument.
411412
412413
Callback function invoked when a promise rejects, receives rejection reason (error) as argument.
413414

415+
#### `onCancel`
416+
417+
> `function(): void`
418+
419+
Callback function invoked when a promise is cancelled, either manually using `cancel()` or automatically due to props
420+
changes or unmounting.
421+
414422
#### `reducer`
415423

416424
> `function(state: any, action: Object, internalReducer: function(state: any, action: Object))`

packages/react-async/src/Async.js

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,16 +65,24 @@ export const createInstance = (defaultProps = {}, displayName = "Async") => {
6565

6666
componentDidUpdate(prevProps) {
6767
const { watch, watchFn = defaultProps.watchFn, promise, promiseFn } = this.props
68-
if (watch !== prevProps.watch) this.load()
69-
if (watchFn && watchFn({ ...defaultProps, ...this.props }, { ...defaultProps, ...prevProps }))
70-
this.load()
68+
if (watch !== prevProps.watch) {
69+
if (this.counter) this.cancel()
70+
return this.load()
71+
}
72+
if (
73+
watchFn &&
74+
watchFn({ ...defaultProps, ...this.props }, { ...defaultProps, ...prevProps })
75+
) {
76+
if (this.counter) this.cancel()
77+
return this.load()
78+
}
7179
if (promise !== prevProps.promise) {
72-
if (promise) this.load()
73-
else this.cancel()
80+
if (this.counter) this.cancel()
81+
if (promise) return this.load()
7482
}
7583
if (promiseFn !== prevProps.promiseFn) {
76-
if (promiseFn) this.load()
77-
else this.cancel()
84+
if (this.counter) this.cancel()
85+
if (promiseFn) return this.load()
7886
}
7987
}
8088

@@ -135,6 +143,8 @@ export const createInstance = (defaultProps = {}, displayName = "Async") => {
135143
}
136144

137145
cancel() {
146+
const onCancel = this.props.onCancel || defaultProps.onCancel
147+
onCancel && onCancel()
138148
this.counter++
139149
this.abortController.abort()
140150
this.mounted && this.dispatch({ type: actionTypes.cancel, meta: this.getMeta() })

packages/react-async/src/specs.js

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@ export const common = Async => () => {
5656
await waitForElement(() => getByText("outer undefined"))
5757
await waitForElement(() => getByText("outer inner"))
5858
})
59+
60+
test("does not cancel on initial mount", async () => {
61+
const onCancel = jest.fn()
62+
const { getByText } = render(<Async onCancel={onCancel}>{() => "done"}</Async>)
63+
await waitForElement(() => getByText("done"))
64+
expect(onCancel).not.toHaveBeenCalled()
65+
})
5966
}
6067

6168
export const withPromise = Async => () => {
@@ -112,29 +119,41 @@ export const withPromise = Async => () => {
112119
})
113120

114121
test("cancels a pending promise when unmounted", async () => {
122+
const onCancel = jest.fn()
115123
const onResolve = jest.fn()
116-
const { unmount } = render(<Async promise={resolveTo("ok")} onResolve={onResolve} />)
124+
const { unmount } = render(
125+
<Async promise={resolveTo("ok")} onCancel={onCancel} onResolve={onResolve} />
126+
)
117127
unmount()
118128
await sleep(10)
129+
expect(onCancel).toHaveBeenCalled()
119130
expect(onResolve).not.toHaveBeenCalled()
120131
})
121132

122133
test("cancels and restarts the promise when `promise` changes", async () => {
123134
const promise1 = resolveTo("one")
124135
const promise2 = resolveTo("two")
136+
const onCancel = jest.fn()
125137
const onResolve = jest.fn()
126-
const { rerender } = render(<Async promise={promise1} onResolve={onResolve} />)
127-
rerender(<Async promise={promise2} onResolve={onResolve} />)
138+
const { rerender } = render(
139+
<Async promise={promise1} onCancel={onCancel} onResolve={onResolve} />
140+
)
141+
rerender(<Async promise={promise2} onCancel={onCancel} onResolve={onResolve} />)
128142
await sleep(10)
143+
expect(onCancel).toHaveBeenCalled()
129144
expect(onResolve).not.toHaveBeenCalledWith("one")
130145
expect(onResolve).toHaveBeenCalledWith("two")
131146
})
132147

133148
test("cancels the promise when `promise` is unset", async () => {
149+
const onCancel = jest.fn()
134150
const onResolve = jest.fn()
135-
const { rerender } = render(<Async promise={resolveTo()} onResolve={onResolve} />)
136-
rerender(<Async onResolve={onResolve} />)
151+
const { rerender } = render(
152+
<Async promise={resolveTo()} onCancel={onCancel} onResolve={onResolve} />
153+
)
154+
rerender(<Async onCancel={onCancel} onResolve={onResolve} />)
137155
await sleep(10)
156+
expect(onCancel).toHaveBeenCalled()
138157
expect(onResolve).not.toHaveBeenCalled()
139158
})
140159

@@ -241,10 +260,11 @@ export const withPromiseFn = (Async, abortCtrl) => () => {
241260
expect(promiseFn).toHaveBeenCalledTimes(1)
242261
fireEvent.click(getByText("increment"))
243262
expect(promiseFn).toHaveBeenCalledTimes(2)
244-
expect(abortCtrl.abort).toHaveBeenCalledTimes(1)
263+
expect(abortCtrl.abort).toHaveBeenCalled()
264+
abortCtrl.abort.mockClear()
245265
fireEvent.click(getByText("increment"))
246266
expect(promiseFn).toHaveBeenCalledTimes(3)
247-
expect(abortCtrl.abort).toHaveBeenCalledTimes(2)
267+
expect(abortCtrl.abort).toHaveBeenCalled()
248268
})
249269

250270
test("re-runs the promise when `watchFn` returns truthy", () => {
@@ -271,31 +291,38 @@ export const withPromiseFn = (Async, abortCtrl) => () => {
271291
expect(promiseFn).toHaveBeenCalledTimes(1)
272292
fireEvent.click(getByText("increment"))
273293
expect(promiseFn).toHaveBeenCalledTimes(1)
274-
expect(abortCtrl.abort).toHaveBeenCalledTimes(0)
294+
expect(abortCtrl.abort).not.toHaveBeenCalled()
275295
fireEvent.click(getByText("increment"))
276296
expect(promiseFn).toHaveBeenCalledTimes(2)
277-
expect(abortCtrl.abort).toHaveBeenCalledTimes(1)
297+
expect(abortCtrl.abort).toHaveBeenCalled()
278298
})
279299

280300
test("cancels a pending promise when unmounted", async () => {
301+
const onCancel = jest.fn()
281302
const onResolve = jest.fn()
282-
const { unmount } = render(<Async promiseFn={() => resolveTo("ok")} onResolve={onResolve} />)
303+
const { unmount } = render(
304+
<Async promiseFn={() => resolveTo("ok")} onCancel={onCancel} onResolve={onResolve} />
305+
)
283306
unmount()
284307
await sleep(10)
308+
expect(onCancel).toHaveBeenCalled()
285309
expect(onResolve).not.toHaveBeenCalled()
286310
expect(abortCtrl.abort).toHaveBeenCalledTimes(1)
287311
})
288312

289313
test("cancels and restarts the promise when `promiseFn` changes", async () => {
290314
const promiseFn1 = () => resolveTo("one")
291315
const promiseFn2 = () => resolveTo("two")
316+
const onCancel = jest.fn()
292317
const onResolve = jest.fn()
293-
const { rerender } = render(<Async promiseFn={promiseFn1} onResolve={onResolve} />)
294-
rerender(<Async promiseFn={promiseFn2} onResolve={onResolve} />)
318+
const { rerender } = render(
319+
<Async promiseFn={promiseFn1} onCancel={onCancel} onResolve={onResolve} />
320+
)
321+
rerender(<Async promiseFn={promiseFn2} onCancel={onCancel} onResolve={onResolve} />)
295322
await sleep(10)
323+
expect(onCancel).toHaveBeenCalled()
296324
expect(onResolve).not.toHaveBeenCalledWith("one")
297325
expect(onResolve).toHaveBeenCalledWith("two")
298-
expect(abortCtrl.abort).toHaveBeenCalledTimes(1)
299326
})
300327

301328
test("cancels the promise when `promiseFn` is unset", async () => {

packages/react-async/src/useAsync.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ const useAsync = (arg1, arg2) => {
8989
}
9090

9191
const cancel = () => {
92+
options.onCancel && options.onCancel()
9293
counter.current++
9394
abortController.current.abort()
9495
isMounted.current && dispatch({ type: actionTypes.cancel, meta: getMeta() })
@@ -99,10 +100,11 @@ const useAsync = (arg1, arg2) => {
99100
if (watchFn && prevOptions.current && watchFn(options, prevOptions.current)) load()
100101
})
101102
useEffect(() => {
102-
promise || promiseFn ? load() : cancel()
103+
if (counter.current) cancel()
104+
if (promise || promiseFn) load()
103105
}, [promise, promiseFn, watch])
104106
useEffect(() => () => (isMounted.current = false), [])
105-
useEffect(() => () => abortController.current.abort(), [])
107+
useEffect(() => () => cancel(), [])
106108
useEffect(() => (prevOptions.current = options) && undefined)
107109

108110
useDebugValue(state, ({ status }) => `[${counter.current}] ${status}`)

0 commit comments

Comments
 (0)