I have a custom hook to help with async queries to an API. The hook works similar to a common useState statement in that you can set an initial value or leave it undefined. In the case of the built-in useState statement, the type of the state is no longer undefined when an initial value is specified (e.g. the type changes from (TType | undefined) to (TType)). In my custom hook, I have an optional parameter for the initial state, but I need to specify the type of the useState in the hook to be (TData | undefined) in case no initiaState is passed in.
But... when an initialState is passed in, I want the type to be only (TData) without the possibility of it being undefined. Otherwise, I need to put checks in place everywhere I use the hook, even when an initial value is set and it will never be undefined.
Is there a way to set the generic type of the useState inside my hook conditionally (i.e. when (initialState !== undefined) then the type is simply (TData), otherwise it is (TData | undefined)?
useAsyncState.ts
import { useCallback, useEffect, useRef, useState } from "react";
interface PropTypes<TData> {
/**
* Promise like async function
*/
asyncFunc?: () => Promise<TData>;
/**
* Initial data
*/
initialState?: TData,
}
/**
* A hook to simplify and combine useState/useEffect statements relying on data which is fetched async.
*
* @example
* const { execute, loading, data, error } = useAync({
* asyncFunc: async () => { return 'data' },
* immediate: false,
* initialData: 'Hello'
* })
*/
const useAsyncState = <TData>({ asyncFunc, initialState }: PropTypes<TData>, dependencies: any[]) => {
// The type of useState should no longer have undefined as an option when initialState !== undefined
const [data, setData] = useState<TData | undefined>(initialState);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<any>(null);
const isMounted = useRef<boolean>(true);
const execute = useCallback(async () => {
if (!asyncFunc) {
return;
}
setLoading(true)
try {
const result = await asyncFunc();
if (!isMounted.current) {
return;
}
setLoading(false);
setData(result);
} catch (err) {
if (!isMounted.current) {
return;
}
setLoading(false);
setError(err);
console.log(err);
}
}, [asyncFunc])
useEffect(() => {
isMounted.current = true;
execute();
return () => {
isMounted.current = false
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [...dependencies])
return {
execute,
loading,
data,
error,
setData,
}
}
export default useAsyncState;
using the hook:
// Currently, the type of data is: number[] | undefined
const { data } = useAsyncState({
asyncFunc: async (): Promise<number[]> => {
return [1, 2, 3]
},
initialState: [],
}, []);
I assume that the useState hook does something similar to what I want to do here as the type of the state is no longer undefined if an initial value is set with useState.