2025-04-01 10:38:02 +09:00

207 lines
5.6 KiB
TypeScript

/**
* Copied from https://github.com/react-hookz/web/blob/579a445fcc9f4f4bb5b9d5e670b2e57448b4ee50/src/useAsync/index.ts
*/
import { useMemo, useRef, useState } from 'react';
import { stringifyErrorLike } from '../utils/misc';
export type AsyncStatus = 'loading' | 'success' | 'error' | 'not-executed';
export type AsyncState<Result> =
| AsyncStateUninitialized<Result>
| AsyncStateFulfilled<Result>
| AsyncStateWithError<Result>
| AsyncStateLoading<Result>;
export type AsyncStateWithError<Result> = {
status: 'error';
error: Error;
result: Result;
};
export type AsyncStateFulfilled<Result> = {
status: 'success';
error: undefined;
result: Result;
};
export type AsyncStateUninitialized<Result> = {
status: 'not-executed';
error: undefined;
result: Result;
};
export type AsyncStateLoading<Result> = {
status: 'loading';
error: Error | undefined;
result: Result;
};
export type UseAsyncActions<Result, Args extends unknown[] = unknown[]> = {
/**
* Reset state to initial.
*/
reset: () => void;
/**
* Execute the async function manually.
*/
execute: (...args: Args) => Promise<Result>;
};
export type UseAsyncMeta<Result, Args extends unknown[] = unknown[]> = {
/**
* Latest promise returned from the async function.
*/
promise: Promise<Result> | undefined;
/**
* List of arguments applied to the latest async function invocation.
*/
lastArgs: Args | undefined;
};
export function useAsync<Result, Args extends unknown[] = unknown[]>(
asyncFn: (...params: Args) => Promise<Result>,
initialValue: Result
): [UseAsyncActions<Result, Args>, AsyncState<Result>, UseAsyncMeta<Result, Args>];
export function useAsync<Result, Args extends unknown[] = unknown[]>(
asyncFn: (...params: Args) => Promise<Result>,
initialValue?: Result
): [UseAsyncActions<Result, Args>, AsyncState<Result | undefined>, UseAsyncMeta<Result, Args>];
/**
* Tracks the result and errors of the provided async function and provides handles to control its execution.
*
* @param asyncFn Function that returns a promise.
* @param initialValue Value that will be set on initialisation before the async function is
* executed.
*/
export function useAsync<Result, Args extends unknown[] = unknown[]>(
asyncFn: (...params: Args) => Promise<Result>,
initialValue?: Result
): [UseAsyncActions<Result, Args>, AsyncState<Result | undefined>, UseAsyncMeta<Result, Args>] {
const [state, setState] = useState<AsyncState<Result | undefined>>({
status: 'not-executed',
error: undefined,
result: initialValue,
});
const promiseRef = useRef<Promise<Result>>();
const argsRef = useRef<Args>();
const methods = useSyncedRef({
execute(...params: Args) {
argsRef.current = params;
const promise = asyncFn(...params);
promiseRef.current = promise;
setState((s) => ({ ...s, status: 'loading' }));
promise.then(
(result) => {
// We dont want to handle result/error of non-latest function
// this approach helps to avoid race conditions
if (promise === promiseRef.current) {
setState((s) => ({ ...s, status: 'success', error: undefined, result }));
}
},
(error: Error) => {
// We dont want to handle result/error of non-latest function
// this approach helps to avoid race conditions
if (promise === promiseRef.current) {
setState((s) => ({ ...s, status: 'error', error }));
}
}
);
return promise;
},
reset() {
setState({
status: 'not-executed',
error: undefined,
result: initialValue,
});
promiseRef.current = undefined;
argsRef.current = undefined;
},
});
return [
useMemo(
() => ({
reset() {
methods.current.reset();
},
execute: (...params: Args) => methods.current.execute(...params),
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
),
state,
{ promise: promiseRef.current, lastArgs: argsRef.current },
];
}
/**
* Like `useRef`, but it returns immutable ref that contains actual value.
*
* @param value
*/
function useSyncedRef<T>(value: T): { readonly current: T } {
const ref = useRef(value);
ref.current = value;
return useMemo(
() =>
Object.freeze({
get current() {
return ref.current;
},
}),
[]
);
}
// --- utility functions to help with request state assertions ---
export function isError<T>(state: AsyncState<unknown>): state is AsyncStateWithError<T> {
return state.status === 'error';
}
export function isSuccess<T>(state: AsyncState<T>): state is AsyncStateFulfilled<T> {
return state.status === 'success';
}
export function isUninitialized<T>(state: AsyncState<T>): state is AsyncStateUninitialized<T> {
return state.status === 'not-executed';
}
export function isLoading<T>(state: AsyncState<T>): state is AsyncStateLoading<T> {
return state.status === 'loading';
}
export function anyOfRequestState(...states: Array<AsyncState<unknown>>) {
return {
uninitialized: states.every(isUninitialized),
loading: states.some(isLoading),
error: states.find(isError)?.error,
success: states.some(isSuccess),
};
}
/**
* This is only used for testing and serializing the async state
*/
export function SerializeState<T>({ state }: { state: AsyncState<T> }) {
return (
<>
{isUninitialized(state) && 'uninitialized'}
{isLoading(state) && 'loading'}
{isSuccess(state) && 'success'}
{isSuccess(state) && `result: ${JSON.stringify(state.result, null, 2)}`}
{isError(state) && `error: ${stringifyErrorLike(state.error)}`}
</>
);
}