In my previous post, we talked about how to replace some component lifecycle functions with useEffect
and useReducer
hooks, while making the resource fetching logic re-usable in the app.
The custom hook we got at the end looks like this:
export const useGet = ({ url }) => {
const [state, dispatch] = useReducer(reducer, {
isLoading: true,
data: null,
error: null,
});
useEffect(() => {
const fetchData = async () => {
dispatch(requestStarted());
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(
`${response.status} ${response.statusText}`
);
}
const data = await response.json();
dispatch(requestSuccessful({ data }));
} catch (e) {
dispatch(requestFailed({ error: e.message }));
}
};
fetchData();
}, [url]);
return state;
};
Looks pretty neat, right? However it has a critical flaw - if the fetch
request is slow, and the component has already unmounted when the async request finishes, you will see this error message from React:
Or - it could have a serious problem - imagine your component that uses this hook received a different ID before the request finishes - so it tries to fetch data from the new url
, and the second request finished just a few ms before the first one - what's gonna happen? Your component will be showing the data from the first request!
The great async/await
might make your code look like it is synchronous, but in reality they are just syntax sugar - your code after await
will still be executed even your component no longer exists on the page. We should always be careful whenever we want to update the state in an asynchronous function.
How do we prevent this from happening? First of all, we should always try to clean up our effects.
The Clean Up Function
If you don't already know - you can return a function at the end of your useEffect
hook. That function will be called whenever that effect is fired again (e.g. when the values of its dependencies have changed), as well as right before the component unmounts. So if you have a useEffect
hook that looks like this:
useEffect(() => {
// logic here
return () => {
// clean up
};
}, []); // no dependencies!
It is actually doing the exact same thing as this code:
class SomeComponent extends React.Component {
componentDidMount() {
// logic here
}
componentWillUnmount() {
// clean up
}
}
If you are attaching an event listener to window
, document
, or some other DOM elements, you can use removeEventListener
in the clean up function to remove them. Similarly, you can clean up setTimeout
/setInterval
with clearTimeout
/clearInterval
.
A Simple Solution
Knowing this, you might think: oh well, that's great, we can set a flag that is set to false when the component unmounts so we can skip all the state updates.
And you are right, that's indeed a very simple solution to this problem:
useEffect(() => {
let isCancelled = false;
const fetchData = async () => {
dispatch(requestStarted());
try {
// fetch logic omitted...
const data = await response.json();
if (!isCancelled) {
dispatch(requestSuccessful({ data }));
}
} catch (e) {
if (!isCancelled) {
dispatch(requestFailed({ error: e.message }));
}
}
};
fetchData();
return () => {
isCancelled = true;
};
}, [url]);
In this code - whenever a new effect runs (or the component unmounts), the previous' effect's isCancelled
is set to true
- and we only update the state when it is false
. This makes sure that your requestSuccessful
and requestFailed
actions are only dispatched on the latest request.
Mission accomplished!...?
But You Really Should Do This
There is a better way though. The code above is fine, however, if your fetch
request is really slow, even if you don't need the results anymore, it is still going on in the background, waiting for a response. Your user might be clicking around and leaving a bunch of stale requests behind - did you know? There is a limit of how many concurrent requests you can have going on at the same time - usually 6 to 8 depending on which browser your users are using. (This applies to HTTP 1.1 only though, things are changing thanks to HTTP/2 and multiplexing, but that's a different topic.) Your stale requests will be blocking newer requests to be executed by the browser, making your app even slower.
Thankfully, there is a new feature in the DOM API called AbortController
which allows you to cancel fetch
requests! It is well supported by most browsers (No IE11 though) and we should definitely take advantage of it.
The AbortController
is very easy to work with. You can create a new one like this:
const myAbortController = new AbortController();
and you will find two fields on the instance: myAbortController.signal
and myAbortController.abort()
. signal
is to be provided to the fetch
call you want to cancel, and when abort
is called that fetch
request will be cancelled.
fetch(url, { signal: myAbortController.signal });
// call the line below to cancel the fetch request above.
myAbortController.abort();
If the request has already completed, abort()
won't do anything.
Awesome, now we can apply this to our hook:
useEffect(() => {
const abortController = new AbortController();
const fetchData = async () => {
dispatch(requestStarted());
try {
fetch(url, { signal: abortController.signal });
// code omitted for brevity
dispatch(requestSuccessful({ data }));
} catch (e) {
dispatch(requestFailed({ error: e.message }));
}
};
fetchData();
return () => {
abortController.abort();
};
}, [url]);
Now our fetch
request will be promptly cancelled for each new effect, or right before the component unmounts.
Handling Cancelled Requests
Just one little thing though - when a request is cancelled it actually throws an error, so our catch
block will be executed. We probably don't want to dispatch a requestFailed
action in this case. Fortunately we can tell if a request has been aborted by checking the signal
on the AbortController
instance.
Let's do that in our catch
block:
try {
// ...
} catch (e) {
// only call dispatch when we know the fetch was not aborted
if (!abortController.signal.aborted) {
dispatch(requestFailed({ error: e.message }));
}
}
Wrapping It Up
Now our hook can properly cleans up after itself! If your hook does something async, in most cases they should be cleaned up properly to avoid any unwanted side-effects.
If you are using fetch
, then abort
your requests in the clean up function. Some third party libraries also provide a way to cancel requests (like the CancelToken
from axios
).
If you want to support older browsers, or your effect doesn't use fetch
, but is using some other async operations (like Promise
), before cancelable Promise
s becomes a reality, use the isCancelled
flag method instead.
Resources
https://developer.mozilla.org/en-US/docs/Web/API/AbortController
Top comments (55)
dispatch
also need to be abortable...dispatch
can be aborted if you useredux-thunk
orredux-saga
- only applies to async actions though.for example:
to use it:
it's not limited to
fetch
oraxios
- 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...
You are using fetch, is it working the same for axios or is there some change?
pretty much the same.
axios
can either use an AbortController or a CancelToken, check their examples here: axios-http.com/docs/cancellationYes 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 :
my action file
and my component
looks like you are calling
abort
already in the dispatch line instead of providing a signal. same to theonClick
handler. I would do this: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
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
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
I managed to make it work, thank you. I was wondering, how do I clear the abort controller once request cancelled ? thanks
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
ok but is there another method, because this one does not work for my use case thanks
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 :
my action file
and my component
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:
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.
Great article! However I don't see why you couldn't use the AbortController even if you're not using
fetch
. You'd just check for theabortController.signal.aborted
flag in whateverresolve
orreject
block you get from thePromise
.Thanks - yes that's possible - however in that case the
AbortController
instance basically acts like a boolean flag. šit can be helpful in certain scenarios though. if you are working with a lot of Promises - I'd recommend checking out npmjs.com/package/cancelable-promise
Whoah, that library looks like just what I was looking for. Thanks!
I am dispatching a thunk call in my useEffect. Now, if I use isCancelled flag as suggested, I want to understand how is it actually preventing state update when my component is unmounted. now, thunk is calling the api in the background, and sets the redux state in the background. now, when happens? When is
if(!isCancelled)
inside the useEffect is being checked?In your case using the flag is not going to work.
If you are using
fetch
to make requests you can use AbortController (just provide the signal to the thunk action)in your request handler's
catch
block, make sure checking if the error is an "AbortError"There are many different implementations for making requests with thunk actions - could you show some code?
my component file has this -
in a diff .js file, I have written the thunk function. which uses axios internally -
Looks like you can just introduce a second parameter to your
thunk
function:now in your component you should provide the cancel token to thunk
so, inside my api function, I am creating new token on every request - if it's the same request though, then it cancels the prev token/req, and re-generate the token and handling everything related to axios inside this function.
So, I will need to change it and basically create token inside useEffect.
Will you be able to redirect me to React docs/github/etc where they suggest this solution? Thank you
No, the token should be created in your
useEffect
call. A new token is created for every new "effect".cancel
/abort
is called whenever the effect re-fires (e.g. when the parameters changed, or when the component unmounts), the cleanup function is called, cancelling the previous request - in your API function you should check if a request has been aborted in yourcatch
block and handle it accordingly.some helpful articles:
reactjs.org/blog/2015/12/16/ismoun...
reactjs.org/docs/hooks-effect.html
github.com/axios/axios#cancellation
But what about other api request especially the event driven ones such as post, patch, put, and delete? I followed this tutorial using axios instead of fetch but i still get the same warning/error. Axios's CancelToken is the same but I can't seem to make a post, patch, put and delete request without re-starting the browser, the initial list component that was fetch in component did mount was unmounted during "isLoading" phase, need help with my code here:
const postRequest = useCallback(() => {
let source = axios.CancelToken.source();
const postData = async (entry) => {
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);
}
}
};
}, []);
Hi - I looked at your program, it doesn't work because the request is never cleaned up. My post talks about automatically clean up requests with
useEffect
indeed it might not be so easy to work with for POST/PUT, etc, or requests that only fire on user action (not via an effect).Your code, uses
useCallback
which is just a simple memoizer. thereturn
at the end won't clean it up for you. We can rewrite it to a function creator that returns a function that automatically cleans up its previous request when called again:to use it:
this function will only allow one request being made at the same time - it doesn't cancel the request for you when the component unmounts though, to do that, we should move
cancelToken
to aref
, here's a possible implementation for that:now we don't need to "make" a new requester anymore, to use it we can call it directly with the new "entry". This
postList
function automatically cancels its previous request when called again, and if there are any pending requests, they will be canceled when the component unmounts.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:
Reducer:
here's my custom hook for all 5 types of api request:
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:
Here's one of the modals for event driven requests like post:
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
orpostRequest
methods are factories - to use them, you have to do something like: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:Please note this method is very different from the first one - to use it, do something like this in the component:
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 yourcatch
blocksaxios.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.
Hi sorry for the late reply, I followed your suggestions and here's what my app can do:
Here's my updated custom hook that has all the factory requests:
I tried negating the: axios.isCancel(err) but to no avail, here's my api request codes.
GET Request:
POST Request:
DELETE Request:
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!
Thanks so much, I added you on github :)
i have implement axios way, but the problem that it cancel all requests immediately and not after leaving the component.
is that a syntax error ?
import { useEffect, useState } from "react";
import Api from "#shared/Api";
import { CancelToken, isCancel } from "axios";
const GetReqhandler = (path) => {
}
export default GetReqhandler;
hmm this looks correct to me - could you setup an code example on codesandbox?
Also this is a custom hook, right?
Yeah, this is a custom hook, here is an example of it's implementation,
i am reformatting code into an old react project, so i tried to implement it to see if it's efficient for the performance, so i can change it in the whole the project (Notice : when i implement it, i didn't change the other normal requests. It might be because of that, i don't know)
Sorry for my late response.
I don't see any obvious errors with the implementation. maybe I'm not getting your question right - are you saying
cancel
is called right away when the component is still mounted? that shouldn't happen since you provided[]
touseEffect
which means it'll only run once and the clean up function is only called when the component unmounts (similar tocomponentDidMount
+componentWillUnmount
).I made something similar to your first example and it works.
codesandbox.io/s/fast-cdn-j37lb?fi...
one quick things though: hook names should start with
use
- instead ofGetReqHandler
you probably want to rename it douseGetReqHandler
.Another thing to note is if you have hot module reloading - your page might unmount and re-mount the component which will cause cancellations - but if you are not doing that, I don't think the problem is here, you might want to check if your code works properly if you change it to a class component.
Yea i thnik this is the problem.
instead of codesandbox here is the project on gitlab
React Project
the useGetReques used under home/abonne/abonne.js
and it's parent is home/home_container.js
if you could suggest a solution to solve this problem using hooks or i any kind of parameters.
Sorry again - would you mind adding me to that project? my gitlab handle is @pallymore
Thanks
I have solve it, i had to change the root component of routes to class instead of fuctional component and it solved the problem
Could you help me how to cancel request using userMeno with dispatch I get this error ( Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function. )
const [stateApp, dispatch] = useReducer(
(stateApp, action) => {
switch (action.type) {
case "SET_APP":
return {
...stateApp,
data: { ...action.payload },
loading: false,
success:true,
error: undefined
};
case "SET_ERROR":
return {
...stateApp,
error: { ...action.payload },
loading:false,
data: {},
success:false
};
default:
return stateApp
}
},
{
data: {},
error: undefined,
loading: true,
success: false
},
)
Hi - I'd recommend not to use
useMemo
in this case - it should be reserved for memoizing results from expensive operations - not functions!useMemo
should be avoided unless you absolutely need it for performance reasons (in real life this doesn't happen very often).Back to the topic - in your case your method is not cancelled when the component unmounts - the right solution would be creating a cancel token (or AbortController if you are using
fetch
) and provide the signal to yourauthentication.get
method - and then incatch
make sure you check for AbortErrors. This depends on the implementation of yourauthentication.get
method.Alternatively - you can do something like this:
please note the request is not aborted unless you provide the
signal
to yourfetch
call. this code here only prevents react from performing state updates.This is a wonderfully flexible solution! Thanks a lot! I understood useReducer much better. But I got a problem. I carefully copied the code and I am getting Maximum update depth exceeded error. This error occurs due to useEffect dependencies on url. If I set [ ] empty array in dependencies all ok, I get my remote data. What am I missing?
useFetch.js
And use it in App.js
sorry for the late response - in your code you are declaring dependency on
options
- which is an object, since react only does shallow comparison, if your options are not memoized, it will triggeruseEffect
on every render (and cancel previous requests).to solve this - I'd recommend declaring an explicit list of simple values that you support in
options
(instead of accepting everything)also I'd remove the default value
options = {}
- since react will create a fresh object{}
on every render, causinguseEffect
to fire unnecessarily.how does this work with axios?
axios
has this thing calledCancelToken
: github.com/axios/axios#cancellationit is very similar to
AbortController
šhowever I would not use
axios
in the front end though.fetch
is very easy to work with - if you want some of theaxios
' default behaviors (throw on 4xx/5xx, returns data by default) you can easily wrapfetch
in your own helper function to do that.Why are you not recommending using axios?
Because
fetch
is already pretty good. I'm not againstaxios
- if you know what you are doing. For any new devs I'd highly recommend learning all the basic DOM APIs and utilities instead of trying to find a third party library for everything.Cool, yeah, best tip, learn all basic DOM API, I'm currently doing this one.
Tbh, this is a very underrated tip but very helpful in the long run.
A complete guide here: wareboss.com/react-hook-clean-up-u...
How does this works with redux and redux-saga? is it advisable to clean up on every fetch?
If you are using
takeLatest
-redux-saga
already cancels the effect for you. If you want to abort the request as well, try this:If you want to cancel sagas manually, check out their cancellation documentation:
redux-saga.js.org/docs/advanced/Ta...
link: github.com/redux-saga/redux-saga/i...
Cool but how do you test it?
Something like this:
I think that might work - however it seems to be testing implementation details - which might be ok if your hook does nothing when aborted.
For testing with
fetch
I usually use something likesinon
's fakeServer. You can intercept requests but not respond to it - unmount the component (or anything that triggers an abort) and check if corresponding side effects are firing (or not firing - e.g. no actions were dispatched)