Reputation: 221
I am using this custom http hook inside different components.
function useHttp(requestFunction, startWithPending = false) {
const httpState = useSelector((state) => state.http);
const dispatch = useDispatch();
const sendRequest = useCallback(
async function (requestData) {
dispatch(httpActions.loading(startWithPending));
try {
const responseData = await requestFunction(requestData);
dispatch(httpActions.success(responseData));
} catch (error) {
dispatch(httpActions.error(error.message || "Something went wrong!!!"));
}
},
[dispatch, requestFunction, startWithPending]
);
return {
sendRequest,
...httpState,
};
}
And I am managing states using Redux toolkit (my http actions).
const initialHttpState = {
status: null,
data: null,
error: null,
};
const httpSlice = createSlice({
name: 'http',
initialState: initialHttpState,
reducers: {
loading(state, action){
state.status = action.payload ? 'pending' : 'null';
state.data = null;
state.error = null;
},
success(state, action){
state.status = 'completed';
state.data = action.payload;
state.error = 'null'
},
error(state, action){
state.status = 'error';
state.data = null;
state.error = action.payload;
}
}
})
And I am using this hook inside useEffect's of multiple components:
const { sendRequest, data, status, error } = useHttp(getProducts, true);
useEffect(() => {
sendRequest();
}, [sendRequest]);
My problem is since different components use the same hook, whenever I tried to call this custom hook inside a component, any other component who uses this same hook get's re-rendered and it causes my app to crash (can't read properties of undefined error) because as you see I'm overwriting response data to the same place and also status are changing.
How can I avoid this re-render problem? Inside my component how can I check whether this re-render demand comes from other components or the component itself? Or how can I change my state management structure to handle this problem?
Upvotes: 2
Views: 925
Reputation: 202836
This is one of those situations where using a single global state isn't exactly what you want from a hook. Each useHttp
hook should have its own internal state for managing the requests and offload the responsibility for what to do with any response values. It is a fairly trivial conversion from Redux code to using the useReducer
hook so each useHttp
hook has its own state and reducer logic.
Example:
const initialHttpState = {
status: null,
data: null,
error: null,
};
const reducer = (state, action) => {
switch(action.type) {
case "loading":
return {
status: action.payload ? 'pending': null,
data: null,
error: null,
};
case "success":
return {
...state,
status: 'completed',
data: action.payload,
error: null,
};
case "error":
return {
...state,
status: 'error',
data: null,
error: action.payload,
};
default:
return state;
}
};
...
const useHttp = (requestFunction, startWithPending = false) => {
const [httpState, dispatch] = React.useReducer(reducer, initialHttpState);
const sendRequest = useCallback(
async function (requestData) {
dispatch(httpActions.loading(startWithPending));
try {
const responseData = await requestFunction(requestData);
dispatch(httpActions.success(responseData));
} catch (error) {
dispatch(httpActions.error(error.message || "Something went wrong!!!"));
}
},
[dispatch, requestFunction, startWithPending]
);
return {
sendRequest,
...httpState,
};
}
Upvotes: 2
Reputation: 234
you can use a key for each request and send it in the payload to update a specific key in the slice
here is an example:
const initialHttpState = {
status: null,
data: null,
error: null,
};
const httpSlice = createSlice({
name: 'http',
initialState: {},
reducers: {
loading(state, action){
if(!state[action.payload.key]) state[action.payload.key] = initialHttpState;
state[action.payload.key].status = action.payload.startWithPending ? 'pending' : 'null';
state[action.payload.key].data = null;
state[action.payload.key].error = null;
},
success(state, action){
state[action.payload.key].status = 'completed';
state[action.payload.key].data = action.payload.responseData;
state[action.payload.key].error = 'null'
},
error(state, action){
state[action.payload.key].status = 'error';
state[action.payload.key].data = null;
state[action.payload.key].error = action.payload.error;
}
}
})
in the hook you will pass the key:
function useHttp(requestFunction, startWithPending = false, key) {
const httpState = useSelector((state) => state.http[key]);
const dispatch = useDispatch();
const sendRequest = useCallback(
async function (requestData) {
dispatch(httpActions.loading({key, startWithPending}));
try {
const responseData = await requestFunction(requestData);
dispatch(httpActions.success({key, responseData}));
} catch (error) {
dispatch(httpActions.error({key, error: error.message || "Something went wrong!!!"}));
}
},
[dispatch, requestFunction, startWithPending]
);
return {
sendRequest,
...httpState,
};
}
therefore when using this hook you will add a key:
const { sendRequest, data, status, error } = useHttp(getProducts, true, 'getProducts');
useEffect(() => {
sendRequest();
}, [sendRequest]);
now your http state should look like this:
{
getProducts: { //http state here },
getUsers: { // http state here }
...
}
and because the updates in state targets a known key and not the entire state therefore it should not rerender your component
PS: : this is an anwser the question asked, but powerful libraries that do the same thing with extra functionalities and performance already exist like rtk query
and react-query
Upvotes: 1