DEV Community

Discussion on: Clean Up Async Requests in `useEffect` Hooks

Collapse
 
mav1283 profile image
Paolo • Edited

I followed the new one but i couldn't make any request for the first approach, It's a simple mern stack, I'm using context api + useReducer:

Actions:

const loading = () => {
  return {
    type: LOADING,
  };
};

const processingRequest = (params) => {
  return {
    type: PROCESSING_REQUEST,
    response: params,
  };
};

const handlingError = () => {
  return {
    type: HANDLING_ERROR,
  };
};

Reducer:

export const initialState = {
  isError: false,
  isLoading: false,
  data: [],                   
};

const listReducer = (state, { type, response }) => {
  switch (type) {
    case LOADING:
      return {
        ...state,
        isLoading: true,
        isError: false,
      };
    case PROCESSING_REQUEST:
      return {
        ...state,
        isLoading: false,
        isError: false,
        data: response,
      };
    case HANDLING_ERROR:
      return {
        ...state,
        isLoading: false,
        isError: true,
      };
    default:
      return state;
  }
};

here's my custom hook for all 5 types of api request:

 const useApiReq = () => {
  const [state, dispatch] = useReducer(listReducer, initialState);

  const getRequest = () => {
    let source;
    return async () => {
      if (source) {
        source.cancel();
      }

      source = axios.CancelToken.source();

      dispatch(loading());
      try {
        const response = await axios.get('/list', {
          cancelToken: source.token,
        });
        dispatch(processingRequest(response.data));
      } catch (err) {
        if (axios.isCancel(err)) {
          dispatch(handlingError);
        }
      }
    };
  };

  const postRequest = (entry) => {
    let source;
    return async (entry) => {
      if (source) {
        source.cancel();
      }

      source = axios.CancelToken.source();

      dispatch(loading());
      try {
        const response = await axios.post(
          '/list',
          {
            cancelToken: source.token,
          },
          entry
        );
        dispatch(processingRequest(response.data));
      } catch (err) {
        if (axios.isCancel(err)) {
          dispatch(handlingError);
        }
      }
    };
  };

  const patchRequest = (id, updated_entry) => {
    let source;
    return async (id, updated_entry) => {
      if (source) {
        source.cancel();
      }

      source = axios.CancelToken.source();

      dispatch(loading());
      try {
        const response = await axios.patch(
          `/list/${id}`,
          {
            cancelToken: source.token,
          },
          updated_entry
        );
        dispatch(processingRequest(response.data));
      } catch (err) {
        if (axios.isCancel(err)) {
          dispatch(handlingError);
        }
      }
    };
  };

  const putRequest = (id, updated_entry) => {
    let source;
    return async (id, updated_entry) => {
      if (source) {
        source.cancel();
      }

      source = axios.CancelToken.source();

      dispatch(loading());
      try {
        const response = await axios.put(
          `/list/${id}`,
          {
            cancelToken: source.token,
          },
          updated_entry
        );
        dispatch(processingRequest(response.data));
      } catch (err) {
        if (axios.isCancel(err)) {
          dispatch(handlingError);
        }
      }
    };
  };

  const deleteRequest = (id) => {
    let source;
    return async (id) => {
      if (source) {
        source.cancel();
      }

      source = axios.CancelToken.source();
      dispatch(loading());
      try {
        const response = await axios.delete(`/list/${id}`, {
          cancelToken: source.token,
        });
        dispatch(processingRequest(response.data));
      } catch (err) {
        if (axios.isCancel(err)) {
          dispatch(handlingError);
        }
      }
    };
  };

  return [
    state,
    getRequest,
    postRequest,
    patchRequest,
    putRequest,
    deleteRequest,
  ];
};

I'm using the custom hooks as values to the context api, here's the main component where the list component is unmounted during the "isLoading" phase, as you can see the get request is inside the useEffect:

function Main() {
  const { state, getRequest } = useContext(AppContext);
  const { isError, isLoading, data } = state;

  useEffect(() => {
    getRequest();
  }, [getRequest]);

  return (
    <main className='App-body'>
      <Sidebar />
      <div className='list-area'>
        {isLoading && (
          <p className='empty-notif'>Loading data from the database</p>
        )}
        {isError && <p className='empty-notif'>Something went wrong</p>}
        {data.length == 0 && <p className='empty-notif'>Database is empty</p>}
        <ul className='parent-list'>
          {data.map((list) => (
            <ParentListItem key={list._id} {...list} />
          ))}
        </ul>
      </div>
    </main>
  );
}

Here's one of the modals for event driven requests like post:

const AddList = ({ exitHandler }) => {
  const { postRequest } = useContext(AppContext);
  const [newList, setNewList] = useState({});
  const inputRef = useRef(null);

  /* On load set focus on the input */
  useEffect(() => {
    inputRef.current.focus();
  }, []);

  const handleAddList = (e) => {
    e.preventDefault();
    const new_list = {
      list_name: inputRef.current.value,
      list_items: [],
    };
    setNewList(new_list);
  };

  const handleSubmit = (e) => {
    e.preventDefault();

    // post request here
    postRequest(newList);
    exitHandler();
  };

  return (
    <form onSubmit={handleSubmit} className='generic-form'>
      <input
        type='text'
        ref={inputRef}
        placeholder='List Name'
        onChange={handleAddList}
      />
      <input type='submit' value='ADD' className='btn-rec' />
    </form>
  );
};
Thread Thread
 
pallymore profile image
Yurui Zhang • Edited

Hi - that's a lot of code - i took a quick look, one thing I probably didn't explain well with my first example is that the makeRequest method returns a function that makes requests when called. (i know it's a bit confusing)

in your example, your getRequest or postRequest methods are factories - to use them, you have to do something like:

// in the component
const { state, getRequest } = useContext(AppContext);
const getList = React.useRef(getRequest()); // note that `getRequest` is called right away

  useEffect(() => {
    getList.current();  // calls on mount
  }, []);

please try to follow my 2nd example as it cleans up the requests when component unmounts. I'd recommend trying to start small, don't try to get everything working in one go, instead, try to focus on only 1 method, and get it to work correctly (and tested) first.

The new postRequest could look something like this:

  const postRequest = async (entry, cancelToken) => {
      dispatch(loading());
      try {
        const response = await axios.post(
          '/list', entry,
          {
            cancelToken,
          }
        );
        dispatch(processingRequest(response.data));
      } catch (err) {
        if (!axios.isCancel(err)) {
          dispatch(handlingError);
        }
      }
    };
  };

Please note this method is very different from the first one - to use it, do something like this in the component:

  const { postRequest } = useContext(AppContext);
const cancelToken = React.useRef(null);

// this effect cancels the requests when the component unmounts;
React.useEffect(() => {
  return () => {
    if (cancelToken.current) {
       cancelToken.current.cancel();
    }
  };
}, []);

const createNewList = (entry) => {
    // cancels the previous request
    if (cancelToken.current) {
      cancelToken.current.cancel();
    }

    // creates a new token 
    cacelToken.current = axios.CancelToken.source();

    postRequest(entry, cancelToken.current.token);
}

  const handleSubmit = (e) => {
    e.preventDefault();
    createNewList(newList);
    exitHandler();
  };

there were a couple of other issues with your code, for exmaple, axios.post takes configuration as the 3rd parameter, not the 2nd; and in your catch blocks axios.isCancel means the request was canceled (instead of encountered an error) - usually we want to handle error when the request was NOT canceled.

Anyways, try to get a single request working properly first before trying to optimize or generalize your use case, don't worry about separating functionality or abstraction at this stage.

Thread Thread
 
mav1283 profile image
Paolo

Hi sorry for the late reply, I followed your suggestions and here's what my app can do:

  1. GET request on initial load
  2. PATCH and PUT request but have to refresh the page to see the changes,
  3. I cannot POST, DELETE

Here's my updated custom hook that has all the factory requests:

const useApiReq = () => {
  const [state, dispatch] = useReducer(listReducer, initialState);

  const getRequest = async (cancelToken) => {
    dispatch(loading());
    try {
      const response = await axios.get('/list', {
        cancelToken,
      });
      dispatch(processingRequest(response.data));
    } catch (err) {
      if (!axios.isCancel(err)) {
        dispatch(handlingError);
      }
    }
  };

  const postRequest = async (entry, cancelToken) => {
    dispatch(loading());
    try {
      const response = await axios.post('/list', entry, {
        cancelToken,
      });
      dispatch(processingRequest(response.data));
    } catch (err) {
      if (!axios.isCancel(err)) {
        dispatch(handlingError);
      }
    }
  };

  const patchRequest = async (id, updated_entry, cancelToken) => {
    dispatch(loading());
    try {
      const response = await axios.patch(`/list/${id}`, updated_entry, {
        cancelToken,
      });
      dispatch(processingRequest(response.data));
    } catch (err) {
      if (!axios.isCancel(err)) {
        dispatch(handlingError);
      }
    }
  };

  const putRequest = async (id, updated_entry, cancelToken) => {
    dispatch(loading());
    try {
      const response = await axios.put(`/list/${id}`, updated_entry, {
        cancelToken,
      });
      dispatch(processingRequest(response.data));
    } catch (err) {
      if (!axios.isCancel(err)) {
        dispatch(handlingError);
      }
    }
  };

  const deleteRequest = async (id, cancelToken) => {
    dispatch(loading());
    try {
      const response = await axios.delete(`/list/${id}`, {
        cancelToken,
      });
      dispatch(processingRequest(response.data));
    } catch (err) {
      if (!axios.isCancel(err)) {
        dispatch(handlingError);
      }
    }
  };

  return [
    state,
    getRequest,
    postRequest,
    patchRequest,
    putRequest,
    deleteRequest,
  ];
};

export default useApiReq;

I tried negating the: axios.isCancel(err) but to no avail, here's my api request codes.

GET Request:

function Main() {
  const { state, getRequest } = useContext(AppContext);
  const cancelToken = useRef(null);
  const { isError, isLoading, data } = state;

  const getData = () => {
    if (cancelToken.current) {
      cancelToken.current.cancel();
    }

    cancelToken.current = axios.CancelToken.source();
    getRequest(cancelToken.current.token);
  };

  useEffect(() => {
    getData();
  }, []);

  useEffect(() => {
    /* axios cleanup */
    return () => {
      if (cancelToken.current) {
        cancelToken.current.cancel();
      }
    };
  }, []);

  return (
    <main className='App-body'>
      <Sidebar />
      <div className='list-area'>
        {isLoading && (
          <p className='empty-notif'>Loading data from the database</p>
        )}
        {isError && <p className='empty-notif'>Something went wrong</p>}
        {data.length == 0 && <p className='empty-notif'>Database is empty</p>}
        <ul className='parent-list'>
          {data.map((list) => (
            <ParentListItem key={list._id} {...list} />
          ))}
        </ul>
      </div>
    </main>
  );
}

export default Main;

POST Request:

const AddList = ({ exitHandler }) => {
  const { postRequest } = useContext(AppContext);
  const [newList, setNewList] = useState({});
  const cancelToken = useRef(null);
  const inputRef = useRef(null);

  /* On load set focus on the input */
  useEffect(() => {
    inputRef.current.focus();
  }, []);

  useEffect(() => {
    /* clean up axios */
    return () => {
      if (cancelToken.current) {
        cancelToken.current.cancel();
      }
    };
  }, []);

  const handleAddList = (e) => {
    e.preventDefault();
    const new_list = {
      list_name: inputRef.current.value,
      list_items: [],
    };
    setNewList(new_list);
  };

  const createNewList = (entry) => {
    if (cancelToken.current) {
      cancelToken.current.cancel();
    }

    /* create token source */
    cancelToken.current = axios.CancelToken.source();
    postRequest(entry, cancelToken.current.token);
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    createNewList(newList);
    exitHandler();
  };

  return (
    <form onSubmit={handleSubmit} className='generic-form'>
      <input
        type='text'
        ref={inputRef}
        placeholder='List Name'
        onChange={handleAddList}
      />
      <input type='submit' value='ADD' className='btn-rec' />
    </form>
  );
};

export default AddList;

DELETE Request:

const DeleteList = ({ exitHandler }) => {
  const { state, deleteRequest } = useContext(AppContext);
  const { data } = state;
  const cancelToken = useRef(null);
  const selectRef = useRef();
  const [targetListId, setTargetListId] = useState();

  useEffect(() => {
    selectRef.current.focus();
  }, []);

  useEffect(() => {
    /* cleanup axios */
    return () => {
      if (cancelToken.current) {
        cancelToken.current.cancel();
      }
    };
  }, []);

  useEffect(() => {
    setTargetListId(data[0]._id);
  }, [data]);

  const deleteList = (entry) => {
    if (cancelToken.current) {
      cancelToken.current.cancel();
    }

    /* create token source */
    cancelToken.current = axios.CancelToken.source();
    deleteRequest(entry, cancelToken.current.token);
  };

  const handleDeleteList = (e) => {
    e.preventDefault();
    deleteList(targetListId);
    exitHandler();
  };

  const handleChangeList = (e) => {
    setTargetListId(e.target.value);
    console.log(targetListId);
  };

  return (
    <form onSubmit={handleDeleteList} className='generic-form'>
      <label>
        <select
          ref={selectRef}
          value={targetListId}
          onChange={handleChangeList}
          className='custom-select'
        >
          {data.map((list) => (
            <option key={list._id} value={list._id}>
              {list.list_name}
            </option>
          ))}
        </select>
      </label>
      <input type='submit' value='DELETE' className='btn-rec' />
    </form>
  );
};

export default DeleteList;
Thread Thread
 
pallymore profile image
Yurui Zhang

hi @paolo - sorry I just saw this. Could you setup a github repo or add me to your existing one? my github handle is @pallymore

alternatively could you set this up on codesandbox.io ? it'll be easier to read/write code there, thanks!

Thread Thread
 
mav1283 profile image
Paolo

Thanks so much, I added you on github :)