Skip to content

Commit afe0b22

Browse files
committed
Add 'watchFn' for more flexibility in reloading based on prop changes.
1 parent 1c79637 commit afe0b22

File tree

5 files changed

+71
-5
lines changed

5 files changed

+71
-5
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ without assumptions about the shape of your data or the type of request.
4040
- Choose between Render Props, Context-based helper components or the `useAsync` hook
4141
- Provides convenient `isLoading`, `startedAt` and `finishedAt` metadata
4242
- Provides `cancel` and `reload` actions
43-
- Automatic re-run using `watch` prop
43+
- Automatic re-run using `watch` or `watchFn` prop
4444
- Accepts `onResolve` and `onReject` callbacks
4545
- Supports [abortable fetch] by providing an AbortController
4646
- Supports optimistic updates using `setData`
@@ -205,6 +205,7 @@ The shorthand version currently does not support passing additional props.
205205
- `promiseFn` {(props, controller) => Promise} A function that returns a promise; invoked in `componentDidMount` and `componentDidUpdate`; receives component props (object) and AbortController instance as arguments
206206
- `deferFn` {(...args, props, controller) => Promise} A function that returns a promise; invoked only by calling `run(...args)`, with arguments being passed through, as well as component props (object) and AbortController as final arguments
207207
- `watch` {any} Watches this property through `componentDidUpdate` and re-runs the `promiseFn` when the value changes (`oldValue !== newValue`)
208+
- `watchFn` {(props, prevProps) => any} Re-runs the `promiseFn` when this callback returns truthy (called on every update).
208209
- `initialValue` {any} initial state for `data` or `error` (if instance of Error); useful for server-side rendering
209210
- `onResolve` {Function} Callback function invoked when a promise resolves, receives data as argument
210211
- `onReject` {Function} Callback function invoked when a promise rejects, receives error as argument

src/index.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,11 @@ export const createInstance = (defaultProps = {}, displayName = "Async") => {
5656
}
5757

5858
componentDidUpdate(prevProps) {
59-
if (prevProps.watch !== this.props.watch) this.load()
60-
if (prevProps.promiseFn !== this.props.promiseFn) {
61-
if (this.props.promiseFn) this.load()
59+
const { watch, watchFn, promiseFn } = this.props
60+
if (watch !== prevProps.watch) this.load()
61+
if (watchFn && watchFn(this.props, prevProps)) this.load()
62+
if (promiseFn !== prevProps.promiseFn) {
63+
if (promiseFn) this.load()
6264
else this.cancel()
6365
}
6466
}

src/spec.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,33 @@ describe("Async", () => {
129129
expect(abortCtrl.abort).toHaveBeenCalledTimes(2)
130130
})
131131

132+
test("re-runs the promise when 'watchFn' returns truthy", () => {
133+
class Counter extends React.Component {
134+
state = { count: 0 }
135+
inc = () => this.setState(state => ({ count: state.count + 1 }))
136+
render() {
137+
return (
138+
<div>
139+
<button onClick={this.inc}>increment</button>
140+
{this.props.children(this.state.count)}
141+
</div>
142+
)
143+
}
144+
}
145+
const promiseFn = jest.fn().mockReturnValue(resolveTo())
146+
const watchFn = ({ count }, prevProps) => count !== prevProps.count && count === 2
147+
const { getByText } = render(
148+
<Counter>{count => <Async promiseFn={promiseFn} watchFn={watchFn} count={count} />}</Counter>
149+
)
150+
expect(promiseFn).toHaveBeenCalledTimes(1)
151+
fireEvent.click(getByText("increment"))
152+
expect(promiseFn).toHaveBeenCalledTimes(1)
153+
expect(abortCtrl.abort).toHaveBeenCalledTimes(0)
154+
fireEvent.click(getByText("increment"))
155+
expect(promiseFn).toHaveBeenCalledTimes(2)
156+
expect(abortCtrl.abort).toHaveBeenCalledTimes(1)
157+
})
158+
132159
test("runs deferFn only when explicitly invoked, passing arguments, props and AbortController", () => {
133160
let counter = 1
134161
const deferFn = jest.fn().mockReturnValue(resolveTo())

src/useAsync.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ const useAsync = (opts, init) => {
44
const counter = useRef(0)
55
const isMounted = useRef(true)
66
const lastArgs = useRef(undefined)
7+
const prevOptions = useRef(undefined)
78
const abortController = useRef({ abort: () => {} })
89

910
const options = typeof opts === "function" ? { promiseFn: opts, initialValue: init } : opts
10-
const { promiseFn, deferFn, initialValue, onResolve, onReject, watch } = options
11+
const { promiseFn, deferFn, initialValue, onResolve, onReject, watch, watchFn } = options
1112

1213
const [state, setState] = useState({
1314
data: initialValue instanceof Error ? undefined : initialValue,
@@ -76,9 +77,13 @@ const useAsync = (opts, init) => {
7677
setState(state => ({ ...state, startedAt: undefined }))
7778
}
7879

80+
useEffect(() => {
81+
if (watchFn && prevOptions.current && watchFn(options, prevOptions.current)) load()
82+
})
7983
useEffect(() => (promiseFn ? load() && undefined : cancel()), [promiseFn, watch])
8084
useEffect(() => () => (isMounted.current = false), [])
8185
useEffect(() => abortController.current.abort, [])
86+
useEffect(() => (prevOptions.current = options) && undefined)
8287

8388
return useMemo(
8489
() => ({

src/useAsync.spec.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,37 @@ describe("useAsync", () => {
127127
expect(abortCtrl.abort).toHaveBeenCalledTimes(2)
128128
})
129129

130+
test("re-runs the promise when 'watchFn' returns truthy", () => {
131+
class Counter extends React.Component {
132+
state = { count: 0 }
133+
inc = () => this.setState(state => ({ count: state.count + 1 }))
134+
render() {
135+
return (
136+
<div>
137+
<button onClick={this.inc}>increment</button>
138+
{this.props.children(this.state.count)}
139+
</div>
140+
)
141+
}
142+
}
143+
const promiseFn = jest.fn().mockReturnValue(resolveTo())
144+
const watchFn = ({ count }, prevProps) => count !== prevProps.count && count === 2
145+
const component = (
146+
<Counter>{count => <Async promiseFn={promiseFn} watchFn={watchFn} count={count} />}</Counter>
147+
)
148+
const { getByText } = render(component)
149+
flushEffects()
150+
expect(promiseFn).toHaveBeenCalledTimes(1)
151+
fireEvent.click(getByText("increment"))
152+
flushEffects()
153+
expect(promiseFn).toHaveBeenCalledTimes(1)
154+
expect(abortCtrl.abort).toHaveBeenCalledTimes(0)
155+
fireEvent.click(getByText("increment"))
156+
flushEffects()
157+
expect(promiseFn).toHaveBeenCalledTimes(2)
158+
expect(abortCtrl.abort).toHaveBeenCalledTimes(1)
159+
})
160+
130161
test("runs deferFn only when explicitly invoked, passing arguments and props", () => {
131162
let counter = 1
132163
const deferFn = jest.fn().mockReturnValue(resolveTo())

0 commit comments

Comments
 (0)