import {
ComponentType,
Dispatch,
ReactNode,
SetStateAction,
createElement,
useCallback,
useState,
} from 'react';
import { createContext, useContextSelector, useContextUpdate } from 'use-context-selector';
export type FetchFunc = (input: Input) => Promise;
export type FetchState = {
input: Input;
result?: Result;
error?: unknown;
promise?: Promise;
}
export const createFetchState = (
fn: FetchFunc,
input: Input,
) => {
const state: FetchState = {
input,
};
state.promise = new Promise((resolve, reject) => {
fn(input).then((r) => {
state.result = r;
resolve();
}).catch((e) => {
state.error = e;
reject(e);
}).finally(() => {
delete state.promise;
});
});
return state;
};
const NO_PROVIDER = 'NO_PROVIDER';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type FetchMap = Map, FetchState>;
type MapState = readonly [
FetchMap,
Dispatch>
]
const FetchContext = createContext(null);
type FetchProviderProps = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any, react/require-default-props
initialInputs?: Iterable, any]>
children: ReactNode;
};
/**
* FetchProvider component
*
* Put this component higher in the component tree.
*
* @example
* import { FetchProvider } from 'react-hooks-fetch';
*
* const App = () => (
*
* ...
*
* );
*/
export const FetchProvider = ({ initialInputs, children }: FetchProviderProps) => {
const createMap = () => {
const map = new Map();
if (initialInputs) {
for (const [fn, input] of initialInputs) {
map.set(fn, createFetchState(fn, input));
}
}
return map;
};
return createElement(
FetchContext.Provider as ComponentType<{ value: MapState }>,
{ value: useState(createMap) },
children,
);
};
export const useFetchState = (
fn: FetchFunc,
) => {
const state = useContextSelector(
FetchContext,
(mapState) => {
if (!mapState) {
return NO_PROVIDER;
}
return mapState[0].get(fn) as FetchState | undefined;
},
);
if (state === NO_PROVIDER) {
throw new Error('missing Provider');
}
return state;
};
export const useSetFetchState = (
fn: FetchFunc,
) => {
const update = useContextUpdate(FetchContext);
const setMapState = useContextSelector(
FetchContext,
(mapState) => {
if (!mapState) {
return NO_PROVIDER;
}
return mapState[1];
},
);
if (setMapState === NO_PROVIDER) {
throw new Error('missing Provider');
}
const setFetchState = useCallback((
nextState: FetchState,
) => {
update(() => {
setMapState((prev) => new Map(prev).set(fn, nextState));
}, { suspense: true });
}, [update, setMapState, fn]);
return setFetchState;
};