I face this problem when I try to setup an interface to the state when trying to use useReducer.I use it in a custom hook for fetching data from the server. The data object in response from the server may contain different properties. the useFetcher.ts
const initialState = {
data: null,
loading: false,
error: null,
};
type LIST_RESPONSE = {
page: number;
totalRows: number;
pageSize: number;
totalPages: number;
list: [];
};
interface State {
data: LIST_RESPONSE| null;
loading: boolean;
error: string | null;
}
const reducer = (state: State, action: ACTIONTYPES) => {
switch (action.type) {
case ACTIONS.FETCHING: {
return {
...state,
loading: true,
};
}
case ACTIONS.FETCH_SUCCESS: {
return {
...state,
loading: false,
data: action.data,
};
}
case ACTIONS.ERROR: {
return {
...state,
loading: false,
error: action.error,
};
}
}
};
const useFetcher = (url: string): State => {
const [state, dispatch] = useReducer(reducer, initialState);
const { data, loading, error } = state;
useEffect(() => {
dispatch({ type: ACTIONS.FETCHING });
const fetchData = async () => {
try {
const response = await httpService.get(url);
dispatch({ type: ACTIONS.FETCH_SUCCESS, data: response.data });
return;
} catch (error: unknown) {
const { message } = error as Error;
dispatch({ type: ACTIONS.ERROR, error: message });
}
};
fetchData();
}, [url]);
return { data, loading, error };
};
export default useFetcher;
Component A call useFetcher:
const { data, loading, error } = useFetcher('productList');
this works fine, it returns the data object that fits the LIST_RESPONSE type. but if I want want to use the useFetcher function in Component B, it is expected to return another data type, let's say like this:
type CATEGORTY_RESPONSE= {
categories: [];
};
How can I make the data type in the interface State generic? for example, I can define the data type in the State interface when I am calling the useFecher hook like this: component A:
const { data, loading, error } = useFetcher<LIST_RESPONSE>('productList');
component B:
const { data, loading, error } = useFetcher<CATEGORTY_RESPONSE>('categories');
I tried
interface State {
data: LIST_RESPONSE|CATEGORTY_RESPONSE| null;
loading: boolean;
error: string | null;
}
or
const useFetcher = <Type extends State>(url: string): Type =>{...}
none of them works. Any help?thanks
In order to use a generic, you will need to apply that generic at multiple points in the chain:
useFetcher
hook needs to know what state type it returns.useReducer
hook needs to know the type of the reducer.reducer
needs to know the type of its state and actions.State
needs to know the type for its data
property.data
property.Here's what that looks like:
import {useEffect, useReducer, Reducer} from "react";
enum ACTIONS {
FETCHING = 'FETCHING',
FETCH_SUCCESS = 'FETCH_SUCCESS',
ERROR = 'ERROR'
}
type ACTIONTYPES<DataType> = {
type: ACTIONS.FETCHING
} | {
type: ACTIONS.ERROR;
error: string
} | {
type: ACTIONS.FETCH_SUCCESS;
data: DataType;
}
const initialState = {
data: null,
loading: false,
error: null,
};
interface State<DataType> {
data: DataType | null;
loading: boolean;
error: string | null;
}
const reducer = <DataType extends any>(state: State<DataType>, action: ACTIONTYPES<DataType>) => {
switch (action.type) {
case ACTIONS.FETCHING: {
return {
...state,
loading: true,
};
}
case ACTIONS.FETCH_SUCCESS: {
return {
...state,
loading: false,
data: action.data,
};
}
case ACTIONS.ERROR: {
return {
...state,
loading: false,
error: action.error,
};
}
default: {
return state;
}
}
};
const useFetcher = <DataType extends any>(url: string): State<DataType> => {
const [state, dispatch] = useReducer<Reducer<State<DataType>, ACTIONTYPES<DataType>>>(reducer, initialState);
const { data, loading, error } = state;
useEffect(() => {
dispatch({ type: ACTIONS.FETCHING });
const fetchData = async () => {
try {
const response = await httpService.get(url);
dispatch({ type: ACTIONS.FETCH_SUCCESS, data: response.data });
return;
} catch (error: unknown) {
const { message } = error as Error;
dispatch({ type: ACTIONS.ERROR, error: message });
}
};
fetchData();
}, [url]);
return { data, loading, error };
};
export default useFetcher;
Instead of using Type extends State
, I am using the same generic value throughout, which refers to the type of data
that we get from the server. This is the only type that changes.
The part which is annoying is setting the generic on useReducer
because the generic here needs to be the type of the reducer rather than the type of the state. The react Reducer
type is defined as Reducer<S, A>
where S
is the state type and A
is the action type. So we wind up with Reducer<State<DataType>, ACTIONTYPES<DataType>>
which is the type of your reducer
function, but limited to the specific DataType
.
The useFetcher
hook is fully generic and the API response data could be anything. We are not limited to just LIST_RESPONSE
and CATEGORTY_RESPONSE
.
For example, you could do this:
const { data, loading, error } = useFetcher<string>('someUrl');
and the type of data
will be inferred as string | null
.
There's a problem with your current LIST_RESPONSE
(and CATEGORTY_RESPONSE
) type. You have declared the type of the list
property to be []
which is an empty array. You need to say that it is an array whose elements are a specific type.
If the list item type is always the same then we can define a type for it and use list: ListItem[];
. If the type varies based on the URL then we can use another generic to describe it.
type LIST_RESPONSE<ItemType> = {
page: number;
totalRows: number;
pageSize: number;
totalPages: number;
list: ItemType[];
};
We can then use your useFetcher
hook for any type of list. Like a list of products:
interface Product {
price: number;
}
const { data, loading, error } = useFetcher<LIST_RESPONSE<Product>>('productList');
if ( data ) {
// has type Product
const product = data.list[0];
// has type number
const price = product.price;
}
If that's a bit confusing then you can think of it in steps.
A ProductListResponse
is a specific type of LIST_RESPONSE
where the list items are of the type Product
:
type ProductListResponse = LIST_RESPONSE<Product>
Our 'productList'
endpoint returns data
which has the type ProductListResponse
:
const { data, loading, error } = useFetcher<ProductListResponse>('productList');