import React from 'react'; import { isFunction } from '@aws-amplify/ui'; // async state constants const INITIAL = { hasError: false, isLoading: false, message: undefined }; const LOADING = { hasError: false, isLoading: true, message: undefined }; const ERROR = { hasError: true, isLoading: false }; export interface AsyncReducerState { /** * current value */ value: T; hasError: boolean; isLoading: boolean; /** * error message, if any */ message: string | undefined; } export type AsyncReducer = (prevValue: S, action: A) => Promise; /** * @internal may be updated in future versions * * @description like `useReducer` but make it async * * @example * ```ts * import fetchData from './fetchData'; * * type MyState = { data: string[] | undefined } * const initialState: MyState = { data: undefined } * * type MyAction = { type: 'fetch' | 'clear' } * * const asyncReducer = async (state: MyState, action: MyAction): Promise => { * switch(action.type) { * case 'fetch': * const data = await fetchData(); * return { data: state.data ? state.data.concat(data) : data } * case 'clear': * return { data: undefined } * } * } * * const FetchDataButton = () => { * const [state, dispatch] = useAsyncReducer(asyncReducer, initialState); * * const { value: { data }, isLoading } = state; * * return ( * * ) * } * ``` */ export default function useAsyncReducer( reducer: AsyncReducer, initialValue: T, options?: { onSuccess?: (data: T) => void; onError?: (error: Error) => void; } ): [AsyncReducerState, React.Dispatch] { const [state, setAsyncState] = React.useState>(() => ({ ...INITIAL, value: initialValue, })); const prevValue = React.useRef(initialValue); const pendingId = React.useRef(); const { onSuccess, onError } = options ?? {}; const dispatch: React.Dispatch = React.useCallback( (input) => { const id = Symbol(); pendingId.current = id; setAsyncState(({ value }) => ({ ...LOADING, value })); reducer(prevValue.current, input) .then((value: T) => { if (pendingId.current !== id) return; prevValue.current = value; if (isFunction(onSuccess)) onSuccess(value); setAsyncState({ ...INITIAL, value }); }) .catch((error: Error) => { if (pendingId.current !== id) return; if (isFunction(onError)) onError(error); const { message } = error; setAsyncState(({ value }) => ({ ...ERROR, value, message })); }); }, [onError, onSuccess, reducer] ); return [state, dispatch]; }