Skip to content

Commit ff0e60a

Browse files
committed
✨ add useAsyncTaskReducer
1 parent 88f2482 commit ff0e60a

4 files changed

Lines changed: 98 additions & 7 deletions

File tree

src/hooks/useImperativeAsyncTask.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { useCallback, useMemo, useReducer } from 'react';
1+
import { useCallback } from 'react';
22
import type AsyncTask from './AsyncTask';
33
import isAbortError from './isAbortError';
4-
import { ActionType, getInitialState, reducer } from '../store';
4+
import { ActionType } from '../store';
55
import useAbortController from '../useAbortController';
6+
import useAsyncTaskReducer from '../useAsyncTaskReducer';
67

78
export type ImperativeAsyncTask<Result> = Readonly<{
89
error: Error | null;
@@ -14,11 +15,7 @@ export type ImperativeAsyncTask<Result> = Readonly<{
1415
function useImperativeAsyncTask<Result>(): ImperativeAsyncTask<Result> {
1516
const { signal } = useAbortController();
1617

17-
// Prettier doesn't yet support instatiantion expressions.
18-
// eslint-disable-next-line prettier/prettier
19-
const initialState = useMemo(getInitialState<Result>, []);
20-
21-
const [state, dispatch] = useReducer(reducer<Result>, initialState);
18+
const [state, dispatch] = useAsyncTaskReducer<Result>();
2219

2320
const executeTask = useCallback(async (task: AsyncTask<Result>) => {
2421
dispatch({ type: ActionType.STARTED });

src/useAsyncTaskReducer/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default } from './useAsyncTaskReducer';
2+
export type { AsyncTaskReducer } from './useAsyncTaskReducer';
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/** @jest-environment jsdom */
2+
3+
import { act, renderHook } from '@testing-library/react';
4+
import { ActionType, reducer } from '../store';
5+
import useAsyncTaskReducer from './useAsyncTaskReducer';
6+
7+
jest.mock('../store', () => {
8+
const store = jest.requireActual('../store');
9+
10+
return {
11+
...store,
12+
reducer: jest.fn(store.reducer),
13+
};
14+
});
15+
16+
describe('useAsyncTaskReducer | integration tests', () => {
17+
beforeEach(() => {
18+
(reducer as jest.Mock).mockClear();
19+
});
20+
21+
it('returns the state and dispatch function', () => {
22+
const { result } = renderHook(useAsyncTaskReducer);
23+
24+
expect(result.current[0]).toEqual({
25+
error: null,
26+
result: null,
27+
pendingTasks: 0,
28+
});
29+
30+
act(() => {
31+
result.current[1]({
32+
type: ActionType.STARTED,
33+
});
34+
});
35+
36+
expect(result.current[0]).toEqual({
37+
error: null,
38+
result: null,
39+
pendingTasks: 1,
40+
});
41+
});
42+
43+
describe('when component unmounts', () => {
44+
it("prevents memory leaks and doesn't dispatch actions", () => {
45+
const { result, unmount } = renderHook(useAsyncTaskReducer);
46+
47+
expect(result.current[0]).toEqual({
48+
error: null,
49+
result: null,
50+
pendingTasks: 0,
51+
});
52+
53+
unmount();
54+
55+
act(() => {
56+
result.current[1]({
57+
type: ActionType.STARTED,
58+
});
59+
});
60+
61+
expect(reducer).not.toHaveBeenCalled();
62+
});
63+
});
64+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { useCallback, useMemo, useReducer } from 'react';
2+
import { Action, getInitialState, reducer, State } from '../store';
3+
import useMounted from '../useMounted';
4+
5+
export type AsyncTaskReducer<Result> = readonly [
6+
state: State<Result>,
7+
dispatch: (action: Action<Result>) => void,
8+
];
9+
10+
function useAsyncTaskReducer<Result>(): AsyncTaskReducer<Result> {
11+
const mounted = useMounted();
12+
13+
const [state, dispatch] = useReducer(
14+
// eslint-disable-next-line prettier/prettier
15+
reducer<Result>,
16+
useMemo(getInitialState<Result>, []),
17+
);
18+
19+
const dispatchOnlyIfMounted = useCallback((action: Action<Result>) => {
20+
if (mounted()) {
21+
dispatch(action);
22+
}
23+
}, []);
24+
25+
return [state, dispatchOnlyIfMounted];
26+
}
27+
28+
export default useAsyncTaskReducer;

0 commit comments

Comments
 (0)