Huseyin Sahin
Huseyin Sahin

Reputation: 221

Avoid re-rendering of a component because of a custom hook which is used by other components?

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

Answers (2)

Drew Reese
Drew Reese

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

free bug coding
free bug coding

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

Related Questions