In many of my projects, I have a common pattern which repeats over and over again.
type RequestStatus =
| 'pending'
| 'requesting'
| 'successful'
| 'failed'
type AValue = string | undefined
type OtherValue = number
interface State {
requestStatus: RequestStatus;
aValue?: AValue;
otherValue?: OtherValue;
}
const [requestState, setRequestState] = useState<State>({
requestStatus: 'pending',
aValue: 'aValue',
otherValue: 11
});
const appendToState = (state: Partial<State>) =>
setRequestState(previousState => ({ ...previousState, ...state }));
In such a scenario, I am fetching values and changing the UI based on the requestStatus
and the values.
This repeats over and over again in my projects and in an attempt to reduce the repetitions, I am considering wrapping all this in a hook. My main problem is dealing with types.
This was one of my attempts.
export const useExtendedRequestStatus = <T>(state: T) => {
const [requestState, setRequestState] = useState<{ requestStatus: RequestStatus } & T>({
requestStatus: 'pending',
...state,
});
const appendToState = (state: Partial<T>) =>
setRequestState(previousState => ({ ...previousState, ...state }));
return { requestState, setRequestState, appendToState };
};
And i would implement it like this
const {
requestStatus,
appendToState
} = useExtendedRequestStatus<{ requestStatus: RequestStatus; aValue: AValue }>({
aValue: aValue,
requestStatus: 'successful',
});
It works but not perfectly. There are instances if I am statically type checking the hook, I have to re-define the RequestStatus
type which is already defined in the hook. I am wondering if there is a way, RequestStatus
will still be in the hook when manually type checking without having to re-define it.
I am open to any ideas.
CodePudding user response:
You indeed use a T
generic in your useExtendedRequestStatus
custom hook, which describes the possible members of the state you want to use it with.
But since you specify that the state
argument, when you call your custom hook, is of type T
directly, if you want it to include an initial requestStatus
, then you are forced to also mention this requestStatus
member in your explicit concrete type (of course you could rely on automatic type inference, but I guess you have your reasons to manually type check).
You can easily "embed" this predefined member in your custom hook, and have it available for your argument (same for appendToState
), in a very similar manner you have already done with your inner useState<{ requestStatus: RequestStatus } & T>({...})
: simply specify that your state
argument is also of type T & { requestStatus: RequestStatus }
(or maybe even better T & Partial<{ requestStatus: RequestStatus }>
in case requestStatus
can be initially omitted, as suggested by your initial default value in your custom hook). That way, T
no longer needs to contain the requestStatus
member. Same for appendToState
.
interface IRequestStatus {
requestStatus: RequestStatus;
}
export const useExtendedRequestStatus = <T>(
state: T & Partial<IRequestStatus> // Initial state can contain requestStatus, even if not mentioned in concrete T
) => {
const [requestState, setRequestState] = useState<IRequestStatus & T>({
requestStatus: 'pending',
...state,
});
const appendToState = (state: Partial<T & IRequestStatus>) =>
setRequestState((previousState) => ({ ...previousState, ...state }));
return { requestState, setRequestState, appendToState };
};
Now in your React functional component, you can use it like this:
const { requestState, appendToState } = useExtendedRequestStatus<{
//requestStatus: RequestStatus; // Can now be omitted
aValue: AValue;
}>({
aValue: aValue,
requestStatus: 'successful', // Okay
});
requestState.requestStatus; // Okay
appendToState({
requestStatus: 'successful', // Okay
});
Demo: https://stackblitz.com/edit/react-ts-eyyzhg?file=Hello.tsx