Skip to content

Commit

Permalink
fix: useAsyncFn does not discard old promises and might produce races
Browse files Browse the repository at this point in the history
  • Loading branch information
dubzzz committed Nov 20, 2019
1 parent f84a5c3 commit 022fa0b
Show file tree
Hide file tree
Showing 2 changed files with 37 additions and 3 deletions.
8 changes: 5 additions & 3 deletions src/useAsyncFn.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DependencyList, useCallback, useState } from 'react';
import { DependencyList, useCallback, useState, useRef } from 'react';
import useMountedState from './useMountedState';

export type AsyncState<T> =
Expand Down Expand Up @@ -28,21 +28,23 @@ export default function useAsyncFn<Result = any, Args extends any[] = any[]>(
deps: DependencyList = [],
initialState: AsyncState<Result> = { loading: false }
): AsyncFn<Result, Args> {
const lastCallId = useRef(0);
const [state, set] = useState<AsyncState<Result>>(initialState);

const isMounted = useMountedState();

const callback = useCallback((...args: Args | []) => {
const callId = ++lastCallId.current;
set({ loading: true });

return fn(...args).then(
value => {
isMounted() && set({ value, loading: false });
isMounted() && callId === lastCallId.current && set({ value, loading: false });

return value;
},
error => {
isMounted() && set({ error, loading: false });
isMounted() && callId === lastCallId.current && set({ error, loading: false });

return error;
}
Expand Down
32 changes: 32 additions & 0 deletions tests/useAsyncFn.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,36 @@ describe('useAsyncFn', () => {
});
});
});

it('should only consider last call and discard previous ones', async () => {
const queuedPromises: { id: number; resolve: () => void }[] = [];
const delayedFunction1 = () => {
return new Promise<number>(resolve => queuedPromises.push({ id: 1, resolve: () => resolve(1) }));
};
const delayedFunction2 = () => {
return new Promise<number>(resolve => queuedPromises.push({ id: 2, resolve: () => resolve(2) }));
};

const hook = renderHook<{ fn: () => Promise<number> }, [AsyncState<number>, () => Promise<number>]>(
({ fn }) => useAsyncFn(fn, [fn]),
{
initialProps: { fn: delayedFunction1 },
}
);
act(() => {
hook.result.current[1](); // invoke 1st callback
});

hook.rerender({ fn: delayedFunction2 });
act(() => {
hook.result.current[1](); // invoke 2nd callback
});

act(() => {
queuedPromises[1].resolve();
queuedPromises[0].resolve();
});
await hook.waitForNextUpdate();
expect(hook.result.current[0]).toEqual({ loading: false, value: 2 });
});
});

0 comments on commit 022fa0b

Please sign in to comment.