mirror of
https://github.com/streamich/react-use.git
synced 2026-01-18 14:06:52 +00:00
feat: improve useAsync and useAsyncFn
This commit is contained in:
commit
3ab1d5db3c
@ -1,11 +1,22 @@
|
||||
module.exports = {
|
||||
presets: ['@babel/preset-env', '@babel/preset-react', "@babel/preset-typescript"],
|
||||
env: {
|
||||
test: {
|
||||
plugins: ['dynamic-import-node']
|
||||
},
|
||||
production: {
|
||||
plugins: ['@babel/plugin-syntax-dynamic-import']
|
||||
}
|
||||
presets: [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
targets: {
|
||||
node: "current"
|
||||
}
|
||||
}
|
||||
],
|
||||
"@babel/preset-react",
|
||||
"@babel/preset-typescript"
|
||||
],
|
||||
env: {
|
||||
test: {
|
||||
plugins: ['dynamic-import-node']
|
||||
},
|
||||
production: {
|
||||
plugins: ['@babel/plugin-syntax-dynamic-import']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -131,5 +131,9 @@
|
||||
"tslint --fix -t verbose",
|
||||
"git add"
|
||||
]
|
||||
},
|
||||
"volta": {
|
||||
"node": "10.16.0",
|
||||
"yarn": "1.16.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
175
src/__tests__/useAsync.test.tsx
Normal file
175
src/__tests__/useAsync.test.tsx
Normal file
@ -0,0 +1,175 @@
|
||||
import { useCallback } from 'react';
|
||||
import { cleanup, renderHook } from 'react-hooks-testing-library';
|
||||
import useAsync from '../useAsync';
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
// NOTE: these tests cause console errors.
|
||||
// maybe we should test in a real environment instead
|
||||
// of a fake one?
|
||||
describe('useAsync', () => {
|
||||
it('should be defined', () => {
|
||||
expect(useAsync).toBeDefined();
|
||||
});
|
||||
|
||||
describe('a success', () => {
|
||||
let hook;
|
||||
let callCount = 0;
|
||||
|
||||
const resolver = async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
callCount++;
|
||||
|
||||
const wait = setTimeout(() => {
|
||||
clearTimeout(wait);
|
||||
resolve('yay');
|
||||
}, 0);
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
callCount = 0;
|
||||
hook = renderHook(({ fn }) => useAsync(fn, [fn]), {
|
||||
initialProps: {
|
||||
fn: resolver,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('initially starts loading', () => {
|
||||
expect(hook.result.current.loading).toEqual(true);
|
||||
});
|
||||
|
||||
it('resolves', async () => {
|
||||
expect.assertions(4);
|
||||
|
||||
hook.rerender({ fn: resolver });
|
||||
await hook.waitForNextUpdate();
|
||||
|
||||
expect(callCount).toEqual(1);
|
||||
expect(hook.result.current.loading).toBeFalsy();
|
||||
expect(hook.result.current.value).toEqual('yay');
|
||||
expect(hook.result.current.error).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('an error', () => {
|
||||
let hook;
|
||||
let callCount = 0;
|
||||
|
||||
const rejection = async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
callCount++;
|
||||
|
||||
const wait = setTimeout(() => {
|
||||
clearTimeout(wait);
|
||||
reject('yay');
|
||||
}, 0);
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
callCount = 0;
|
||||
hook = renderHook(({ fn }) => useAsync(fn, [fn]), {
|
||||
initialProps: {
|
||||
fn: rejection,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('initially starts loading', () => {
|
||||
expect(hook.result.current.loading).toBeTruthy();
|
||||
});
|
||||
|
||||
it('resolves', async () => {
|
||||
expect.assertions(4);
|
||||
|
||||
hook.rerender({ fn: rejection });
|
||||
await hook.waitForNextUpdate();
|
||||
|
||||
expect(callCount).toEqual(1);
|
||||
expect(hook.result.current.loading).toBeFalsy();
|
||||
expect(hook.result.current.error).toEqual('yay');
|
||||
expect(hook.result.current.value).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('re-evaluates when dependencies change', () => {
|
||||
describe('the fn is a dependency', () => {
|
||||
let hook;
|
||||
let callCount = 0;
|
||||
|
||||
const initialFn = async () => {
|
||||
callCount++;
|
||||
return 'value';
|
||||
};
|
||||
|
||||
const differentFn = async () => {
|
||||
callCount++;
|
||||
return 'new value';
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
callCount = 0;
|
||||
hook = renderHook(({ fn }) => useAsync(fn, [fn]), {
|
||||
initialProps: { fn: initialFn },
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the first value', () => {
|
||||
expect(hook.result.current.value).toEqual('value');
|
||||
});
|
||||
|
||||
it('renders a different value when deps change', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
expect(callCount).toEqual(1);
|
||||
|
||||
hook.rerender({ fn: differentFn }); // change the fn to initiate new request
|
||||
await hook.waitForNextUpdate();
|
||||
|
||||
expect(callCount).toEqual(2);
|
||||
expect(hook.result.current.value).toEqual('new value');
|
||||
});
|
||||
});
|
||||
|
||||
describe('the additional dependencies list changes', () => {
|
||||
let callCount = 0;
|
||||
let hook;
|
||||
|
||||
const staticFunction = async counter => {
|
||||
callCount++;
|
||||
return `counter is ${counter} and callCount is ${callCount}`;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
callCount = 0;
|
||||
hook = renderHook(
|
||||
({ fn, counter }) => {
|
||||
const callback = useCallback(() => fn(counter), [counter]);
|
||||
return useAsync<string>(callback, [callback]);
|
||||
},
|
||||
{
|
||||
initialProps: {
|
||||
counter: 0,
|
||||
fn: staticFunction,
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('initial renders the first passed pargs', () => {
|
||||
expect(hook.result.current.value).toEqual('counter is 0 and callCount is 1');
|
||||
});
|
||||
|
||||
it('renders a different value when deps change', async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
hook.rerender({ fn: staticFunction, counter: 1 });
|
||||
await hook.waitForNextUpdate();
|
||||
|
||||
expect(hook.result.current.value).toEqual('counter is 1 and callCount is 2');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
98
src/__tests__/useAsyncFn.test.tsx
Normal file
98
src/__tests__/useAsyncFn.test.tsx
Normal file
@ -0,0 +1,98 @@
|
||||
// NOTE: most behavior that useAsyncFn provides
|
||||
// is covered be the useAsync tests.
|
||||
//
|
||||
// The main difference is that useAsyncFn
|
||||
// does not automatically invoke the function
|
||||
// and it can take arguments.
|
||||
|
||||
import { cleanup, renderHook } from 'react-hooks-testing-library';
|
||||
import useAsyncFn, { AsyncState } from '../useAsyncFn';
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
type AdderFn = (a: number, b: number) => Promise<number>;
|
||||
|
||||
describe('useAsyncFn', () => {
|
||||
it('should be defined', () => {
|
||||
expect(useAsyncFn).toBeDefined();
|
||||
});
|
||||
|
||||
describe('the callback can be awaited and return the value', () => {
|
||||
let hook;
|
||||
let callCount = 0;
|
||||
const adder = async (a: number, b: number): Promise<number> => {
|
||||
callCount++;
|
||||
return a + b;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// NOTE: renderHook isn't good at inferring array types
|
||||
hook = renderHook<{ fn: AdderFn }, [AsyncState<number>, AdderFn]>(({ fn }) => useAsyncFn(fn), {
|
||||
initialProps: {
|
||||
fn: adder,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('awaits the result', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const [s, callback] = hook.result.current;
|
||||
|
||||
const result = await callback(5, 7);
|
||||
|
||||
expect(result).toEqual(12);
|
||||
|
||||
const [state] = hook.result.current;
|
||||
|
||||
expect(state.value).toEqual(12);
|
||||
expect(result).toEqual(state.value);
|
||||
});
|
||||
});
|
||||
|
||||
describe('args can be passed to the function', () => {
|
||||
let hook;
|
||||
let callCount = 0;
|
||||
const adder = async (a: number, b: number): Promise<number> => {
|
||||
callCount++;
|
||||
return a + b;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// NOTE: renderHook isn't good at inferring array types
|
||||
hook = renderHook<{ fn: AdderFn }, [AsyncState<number>, AdderFn]>(({ fn }) => useAsyncFn(fn), {
|
||||
initialProps: {
|
||||
fn: adder,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('initially does not have a value', () => {
|
||||
const [state] = hook.result.current;
|
||||
|
||||
expect(state.value).toEqual(undefined);
|
||||
expect(state.loading).toEqual(false);
|
||||
expect(state.error).toEqual(undefined);
|
||||
expect(callCount).toEqual(0);
|
||||
});
|
||||
|
||||
describe('when invoked', () => {
|
||||
it('resolves a value derived from args', async () => {
|
||||
expect.assertions(4);
|
||||
|
||||
const [s, callback] = hook.result.current;
|
||||
|
||||
callback(2, 7);
|
||||
hook.rerender({ fn: adder });
|
||||
await hook.waitForNextUpdate();
|
||||
|
||||
const [state, c] = hook.result.current;
|
||||
|
||||
expect(callCount).toEqual(1);
|
||||
expect(state.loading).toEqual(false);
|
||||
expect(state.error).toEqual(undefined);
|
||||
expect(state.value).toEqual(9);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -18,14 +18,17 @@ export type AsyncState<T> =
|
||||
value: T;
|
||||
};
|
||||
|
||||
const useAsync = <T>(fn: () => Promise<T>, deps: DependencyList = []) => {
|
||||
const [state, callback] = useAsyncFn(fn, deps);
|
||||
export default function useAsync<Result = any, Args extends any[] = any[]>(
|
||||
fn: (...args: Args | []) => Promise<Result>,
|
||||
deps: DependencyList = []
|
||||
) {
|
||||
const [state, callback] = useAsyncFn<Result, Args>(fn, deps, {
|
||||
loading: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
callback();
|
||||
}, [callback]);
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
export default useAsync;
|
||||
}
|
||||
|
||||
@ -18,31 +18,35 @@ export type AsyncState<T> =
|
||||
value: T;
|
||||
};
|
||||
|
||||
const useAsyncFn = <T>(fn: (...args: any[]) => Promise<T>, deps: DependencyList = []): [AsyncState<T>, () => void] => {
|
||||
const [state, set] = useState<AsyncState<T>>({
|
||||
loading: false,
|
||||
});
|
||||
export default function useAsyncFn<Result = any, Args extends any[] = any[]>(
|
||||
fn: (...args: Args | []) => Promise<Result>,
|
||||
deps: DependencyList = [],
|
||||
initialState: AsyncState<Result> = { loading: false }
|
||||
): [AsyncState<Result>, (...args: Args | []) => Promise<Result>] {
|
||||
const [state, set] = useState<AsyncState<Result>>(initialState);
|
||||
|
||||
const mounted = useRefMounted();
|
||||
|
||||
const callback = useCallback((...args) => {
|
||||
const callback = useCallback((...args: Args | []) => {
|
||||
set({ loading: true });
|
||||
|
||||
fn(...args).then(
|
||||
return fn(...args).then(
|
||||
value => {
|
||||
if (mounted.current) {
|
||||
set({ value, loading: false });
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
error => {
|
||||
if (mounted.current) {
|
||||
set({ error, loading: false });
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
);
|
||||
}, deps);
|
||||
|
||||
return [state, callback];
|
||||
};
|
||||
|
||||
export default useAsyncFn;
|
||||
}
|
||||
|
||||
34
tslint.json
34
tslint.json
@ -1,18 +1,17 @@
|
||||
{
|
||||
"defaultSeverity": "error",
|
||||
"extends": [
|
||||
"tslint:recommended",
|
||||
"tslint-react",
|
||||
"tslint-eslint-rules",
|
||||
"tslint-config-prettier"
|
||||
],
|
||||
"linterOptions": {
|
||||
"exclude": [
|
||||
"node_modules/**"
|
||||
]
|
||||
},
|
||||
"jsRules": {},
|
||||
"rules": {
|
||||
"defaultSeverity": "error",
|
||||
"extends": [
|
||||
"tslint:recommended",
|
||||
"tslint-react",
|
||||
"tslint-eslint-rules",
|
||||
"tslint-config-prettier"
|
||||
],
|
||||
"linterOptions": {
|
||||
"exclude": ["node_modules/**"]
|
||||
},
|
||||
"jsRules": {},
|
||||
"rules": {
|
||||
"ban-types": false,
|
||||
"interface-name": [true, "never-prefix"],
|
||||
"no-console": false,
|
||||
"max-classes-per-file": false,
|
||||
@ -32,7 +31,6 @@
|
||||
"tabWidth": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
"rulesDirectory": ["tslint-plugin-prettier"]
|
||||
}
|
||||
|
||||
},
|
||||
"rulesDirectory": ["tslint-plugin-prettier"]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user