DEV Community

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

Collapse
 
maltoze profile image
maltoze

dispatch also need to be abortable...

Collapse
 
pallymore profile image
Yurui Zhang • Edited

dispatch can be aborted if you use redux-thunk or redux-saga - only applies to async actions though.

for example:

const thunkAction = (payload, signal) => {
  return (dispatch) => {
     fetch(payload, { signal }).then(response => {
        dispatch(respondeLoadedAction(response));
     }).catch(error => {
        if (signal.aborted) {
           // aborted
        } else {
           // errored 
           dispatch(errorAction(error));
        }
     });
  };
}
Enter fullscreen mode Exit fullscreen mode

to use it:

const abortController = new AbortController();

const doStuff = (payload) => dispatch(thinkAction(payload, abortController.signal));

doStuff(payload);
// to abort it: call abortController.abort();
Enter fullscreen mode Exit fullscreen mode

it's not limited to fetch or axios - you can use AbortSignal for many things, just be careful it might not throw error automatically when used outside of making requests.

developer.mozilla.org/en-US/docs/W...

Collapse
 
m27 profile image
m27 • Edited

You are using fetch, is it working the same for axios or is there some change?

Thread Thread
 
pallymore profile image
Yurui Zhang

pretty much the same.

axios can either use an AbortController or a CancelToken, check their examples here: axios-http.com/docs/cancellation

Thread Thread
 
m27 profile image
m27 • Edited

Yes I checked their doc, I have trouble cancelling one request via redux. Basically I have my axios in a service file, then I call the axios req in the action. In my component I have a useEffect which runs when require, and save data to api, and whenever I trigger the button, I would like the call to be cancelled. Any help on how to achieve this ? thanks

Here is my service file :



export const sync = (l, obj, pId, controller) => {
  let a = { ...obj };

  return axios.post(`/${l}/f/ff/Create`, JSON.stringify(cv(a, l, pId)), {
    signal: controller.signal
  });
};
Enter fullscreen mode Exit fullscreen mode

my action file

export const save =
    (l, el, pId,controller ) => async (dispatch) => {
        try {
            dispatch({ type: SAVE_REQUEST });

            await sync(l, el, pId, controller);

            dispatch({
                type: SAVE_SUCCESS,
                payload: el,
            });
        } catch ( error) {
                dispatch({ type: SAVE_FAIL, payload: error});
            }
        }
  };

Enter fullscreen mode Exit fullscreen mode

and my component


 const { current: controller } = useRef(new AbortController());

  useEffect(() => {
    dispatch(save(l,el,p.id,controller)
    },[])

const handleAbort=(){
  controller.abort()
}

  <div>
  <button onclick={handleAbort()}>abort</button>
  </div>
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
pallymore profile image
Yurui Zhang

looks like you are calling abort already in the dispatch line instead of providing a signal. same to the onClick handler. I would do this:

  const { current: controller } = useRef(new AbortController());

  useEffect(() => {
    dispatch(save(l,el,p.id, controller.signal)
    },[])

const handleAbort= () => {
  controller.abort()
}

  <div>
  <button onclick={handleAbort}>abort</button>
  </div>
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
m27 profile image
m27 • Edited

it does not seem to work, idk why, is there a chance you can take a look at the service and action file that I posted, just to check if I have made any mistake thanks

Thread Thread
 
pallymore profile image
Yurui Zhang

could you be a bit more specific though 😅 a minimal example would be helpful.

the method explained in the post definitely works - you probably have something else going on in the app

Thread Thread
 
m27 profile image
m27

yes the component seems fine, it is just that in my action and service file, I am not sure I set up the controller properly

Thread Thread
 
m27 profile image
m27

I managed to make it work, thank you. I was wondering, how do I clear the abort controller once request cancelled ? thanks

Thread Thread
 
pallymore profile image
Yurui Zhang

awesome news. 😅

oh yea about that there are different methods. I think the abort controller should only be created before each request. one way of doing that is useRef

const abortCtrlRef = useRef(null);

const handleCancel = useCallback(() => {
  abortCtrlRef.current?.abort();
  abortCtrlRef.current = null;
}, []);

useEffect(() => {
  abortCtrlRef.current = new AbortController();

  makeRequest(abortCtrlRef.current.signal);

  return () => {
    handleCancel();
  };
}, [handleCancel, andOtherDeps]);

Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
m27 profile image
m27 • Edited

ok but is there another method, because this one does not work for my use case thanks

Thread Thread
 
m27 profile image
m27 • Edited

This is what I have so far. I have a modal, this modal is saving data to a database, once I click on the abort, i cancelled the call (to save data) in the useEffect. Let 's say now the user exit the modal and decide some other time to go back on the modal to save its data again, the cancel signal is still on the route. How do I clear up the abort controller ?

Here is my service file :



export const sync = (l, obj, pId, controller) => {
  let a = { ...obj };

  return axios.post(`/${l}/f/ff/Create`, {
    signal: controller.signal
  });
};
Enter fullscreen mode Exit fullscreen mode

my action file

export const save =
    (l, el, pId,controller ) => async (dispatch) => {
        try {
            dispatch({ type: SAVE_REQUEST });

            await sync(l, el, pId, controller);

            dispatch({
                type: SAVE_SUCCESS,
                payload: el,
            });
        } catch ( error) {
                dispatch({ type: SAVE_FAIL, payload: error});
            }
        }
  };

Enter fullscreen mode Exit fullscreen mode

and my component


 const { current: controller } = useRef(new AbortController());

  useEffect(() => {
    dispatch(save(l,el,p.id,controller)
    },[])

const handleAbort=(){
  controller.abort()
}

  <div>
<Modal>
<progressbar/>
  <button onclick={handleAbort()}>abort</button>
<button >exit</button>
</Modal>
  </div>
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
pallymore profile image
Yurui Zhang

in your example it's hard to tell what the issue is or what are you trying to do.

two things that's kinda obvious here:

  1. you are not creating a new AbortController before each request (check my previous example)
  2. your useEffect only fires once (when the component mounts) - the request is not made on demand, which seems to be what you are trying to do here. If you want the request to be made every time the modal is opened, you will need to make sure opening the modal (either rendering the modal, or changing the state) is tied to the effect/request.

also, when you say "it doesn't work" - please elaborate why or how. there could be many reasons for broken code, without errors/logs/descriptions, it's impossible to tell what might be wrong by simply looking at a few lines of code.