React useEffect causing: Can't perform a React state update on an unmounted component
Asked Answered
V

19

188

When fetching data I'm getting: Can't perform a React state update on an unmounted component. The app still works, but react is suggesting I might be causing a memory leak.

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."

Why do I keep getting this warning?

I tried researching these solutions:

https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal

https://developer.mozilla.org/en-US/docs/Web/API/AbortController

but this still was giving me the warning.

const  ArtistProfile = props => {
  const [artistData, setArtistData] = useState(null)
  const token = props.spotifyAPI.user_token

  const fetchData = () => {
    const id = window.location.pathname.split("/").pop()
    console.log(id)
    props.spotifyAPI.getArtistProfile(id, ["album"], "US", 10)
    .then(data => {setArtistData(data)})
  }
  useEffect(() => {
    fetchData()
    return () => { props.spotifyAPI.cancelRequest() }
  }, [])
  
  return (
    <ArtistProfileContainer>
      <AlbumContainer>
        {artistData ? artistData.artistAlbums.items.map(album => {
          return (
            <AlbumTag
              image={album.images[0].url}
              name={album.name}
              artists={album.artists}
              key={album.id}
            />
          )
        })
        : null}
      </AlbumContainer>
    </ArtistProfileContainer>
  )
}

Edit:

In my api file I added an AbortController() and used a signal so I can cancel a request.

export function spotifyAPI() {
  const controller = new AbortController()
  const signal = controller.signal

// code ...

  this.getArtist = (id) => {
    return (
      fetch(
        `https://api.spotify.com/v1/artists/${id}`, {
        headers: {"Authorization": "Bearer " + this.user_token}
      }, {signal})
      .then(response => {
        return checkServerStat(response.status, response.json())
      })
    )
  }

  // code ...

  // this is my cancel method
  this.cancelRequest = () => controller.abort()
}

My spotify.getArtistProfile() looks like this

this.getArtistProfile = (id,includeGroups,market,limit,offset) => {
  return Promise.all([
    this.getArtist(id),
    this.getArtistAlbums(id,includeGroups,market,limit,offset),
    this.getArtistTopTracks(id,market)
  ])
  .then(response => {
    return ({
      artist: response[0],
      artistAlbums: response[1],
      artistTopTracks: response[2]
    })
  })
}

but because my signal is used for individual api calls that are resolved in a Promise.all I can't abort() that promise so I will always be setting the state.

Venola answered 2/3, 2019 at 1:25 Comment(4)
The warning is because the Promise getArtistProfile() returns resolves after the component has unmounted. Either cancel that request, or if that's not possible add a check in the .then() handler so setArtistData() is not called if the component has been unmountedFloorer
It will not be possible to explain why it is happening without knowing more about your application outside of this component. We need to know what causes this component to mount/unmount. What is happening in the application when you get the error?Apperception
@ııı How would I check if the component has unmounted?Venola
This is not a real memory leak, but most likely a false warning - which is why the React team will remove the warning in the next release. See PRCartilage
F
59

Sharing the AbortController between the fetch() requests is the right approach.
When any of the Promises are aborted, Promise.all() will reject with AbortError:

function Component(props) {
  const [fetched, setFetched] = React.useState(false);
  React.useEffect(() => {
    const ac = new AbortController();
    Promise.all([
      fetch('http://placekitten.com/1000/1000', {signal: ac.signal}),
      fetch('http://placekitten.com/2000/2000', {signal: ac.signal})
    ]).then(() => setFetched(true))
      .catch(ex => console.error(ex));
    return () => ac.abort(); // Abort both fetches on unmount
  }, []);
  return fetched;
}
const main = document.querySelector('main');
ReactDOM.render(React.createElement(Component), main);
setTimeout(() => ReactDOM.unmountComponentAtNode(main), 1); // Unmount after 1ms
<script src="//cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.development.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.development.js"></script>
<main></main>
Floorer answered 2/3, 2019 at 23:52 Comment(0)
C
139

For me, clean the state in the unmount of the component helped.

 const [state, setState] = useState({});

useEffect(() => {
    myFunction();
    return () => {
      setState({}); // This worked for me
    };
}, []);

const myFunction = () => {
    setState({
        name: 'Jhon',
        surname: 'Doe',
    })
}

Crescint answered 25/11, 2020 at 15:25 Comment(9)
I dont understand the logic behind but it works.Rootless
Oh, I think I got it. Callback function in useEffect will be executed only when component will be unloaded. That's why we can access name and surname props of state before component will be unloaded.Metaprotein
When you return a function from useEffect, that function will will be executed when the component unmounts. So taking advantage of that, you set your state to an empty. Doing this, whenever you leave that screen or the component unmounts, the state will be empty, so the components of your screen won't be trying to re-render again. I hope this helpsEng
this would have worked even if you return an empty function from useEffect. React just ensures that you are returning a function from useEffect to perform cleanup. it doesnt care what cleanup you performAdolfoadolph
@Adolfoadolph For me it was not the case, only "undefining" the state eliminated the issueOperate
Thanks man. Don't know how but this works. but you said clean in the unmount. This gives me some hints. I will try to learn about the useEffect.Awfully
In many cases, by just returning an empty function in useEffect(), you make React think you're cleaning up any side effects, it will fix the unmount component error.Sphenic
A similar implementation worked for me as well, only instead of a generic state and object, I had multiple state attributes I tracked. In any event, using a return that set each state to the same as it's created with fixed it (i.e. if the state boolean is useState(false) you set the same with the functional method call).Scilla
This created a new error for me: "ERROR Warning: Internal React error: Attempted to capture a commit phase error inside a detached tree. This indicates a bug in React. Likely causes include deleting the same fiber more than once, committing an already-finished tree, or an inconsistent return pointer."Victual
F
59

Sharing the AbortController between the fetch() requests is the right approach.
When any of the Promises are aborted, Promise.all() will reject with AbortError:

function Component(props) {
  const [fetched, setFetched] = React.useState(false);
  React.useEffect(() => {
    const ac = new AbortController();
    Promise.all([
      fetch('http://placekitten.com/1000/1000', {signal: ac.signal}),
      fetch('http://placekitten.com/2000/2000', {signal: ac.signal})
    ]).then(() => setFetched(true))
      .catch(ex => console.error(ex));
    return () => ac.abort(); // Abort both fetches on unmount
  }, []);
  return fetched;
}
const main = document.querySelector('main');
ReactDOM.render(React.createElement(Component), main);
setTimeout(() => ReactDOM.unmountComponentAtNode(main), 1); // Unmount after 1ms
<script src="//cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.development.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.development.js"></script>
<main></main>
Floorer answered 2/3, 2019 at 23:52 Comment(0)
S
56

For example, you have some component that does some asynchronous actions, then writes the result to state and displays the state content on a page:

export default function MyComponent() {
    const [loading, setLoading] = useState(false);
    const [someData, setSomeData] = useState({});
    // ...
    useEffect( () => {
        (async () => {
            setLoading(true);
            someResponse = await doVeryLongRequest(); // it takes some time
            // When request is finished:
            setSomeData(someResponse.data); // (1) write data to state
            setLoading(false); // (2) write some value to state
        })();
    }, []);

    return (
        <div className={loading ? "loading" : ""}>
            {someData}
            <Link to="SOME_LOCAL_LINK">Go away from here!</Link>
        </div>
    );
}

Let's say that user clicks some link when doVeryLongRequest() still executes. MyComponent is unmounted but the request is still alive and when it gets a response it tries to set state in lines (1) and (2) and tries to change the appropriate nodes in HTML. We'll get an error from subject.

We can fix it by checking whether compponent is still mounted or not. Let's create a componentMounted ref (line (3) below) and set it true. When component is unmounted we'll set it to false (line (4) below). And let's check the componentMounted variable every time we try to set state (line (5) below).

The code with fixes:

export default function MyComponent() {
    const [loading, setLoading] = useState(false);
    const [someData, setSomeData] = useState({});
    const componentMounted = useRef(true); // (3) component is mounted
    // ...
    useEffect( () => {
        (async () => {
            setLoading(true);
            someResponse = await doVeryLongRequest(); // it takes some time
            // When request is finished:
            if (componentMounted.current){ // (5) is component still mounted?
                setSomeData(someResponse.data); // (1) write data to state
                setLoading(false); // (2) write some value to state
            }
            return () => { // This code runs when component is unmounted
                componentMounted.current = false; // (4) set it to false when we leave the page
            }
        })();
    }, []);

    return (
        <div className={loading ? "loading" : ""}>
            {someData}
            <Link to="SOME_LOCAL_LINK">Go away from here!</Link>
        </div>
    );
}
Sexagenarian answered 31/3, 2021 at 16:46 Comment(7)
I'm not confident on this information, but setting the componentMounted variable that way will probably trigger the following warning: "Assignments to the 'componentMounted' variable from inside React Hook useEffect will be lost after each render. To preserve the value over time, store it in a useRef Hook and keep the mutable value in the '.current' property. ..." In that case, setting it as a state might be necessary as advised here: #56156459Sludge
It is valid but you should use the useRef hook to store the value of componentMounted (mutable value) or move the declaration of the componentMounted variable inside the useEffectSelfinterest
Agree, guys. FixedSexagenarian
Don't the useEffect need an async callback to use the await at someResponse? useEffect(async () => {...},[])Analgesic
Thanks, Luigi, you're right. FixedSexagenarian
Just another detail, doesn't the useEffect should be a "normal" function without the async? And the async call should be done via a function defined inside/outside of the useEffect body? i.e. useEffect( () => { const doStuff = async () => { ... }; doStuff(); ... }, []);Lotty
Gotcha! :) Thanks, vicvans20, fixedSexagenarian
M
14

Why do I keep getting this warning?

The intention of this warning is to help you prevent memory leaks in your application. If the component updates it's state after it has been unmounted from the DOM, this is an indication that there could be a memory leak, but it is an indication with a lot of false positives.

How do I know if I have a memory leak?

You have a memory leak if an object that lives longer than your component holds a reference to it, either directly or indirectly. This usually happens when you subscribe to events or changes of some kind without unsubscribing when your component unmounts from the DOM.

It typically looks like this:

useEffect(() => {
  function handleChange() {
     setState(store.getState())
  }
  // "store" lives longer than the component, 
  // and will hold a reference to the handleChange function.
  // Preventing the component to be garbage collected after 
  // unmount.
  store.subscribe(handleChange)

  // Uncomment the line below to avoid memory leak in your component
  // return () => store.unsubscribe(handleChange)
}, [])

Where store is an object that lives further up the React tree (possibly in a context provider), or in global/module scope. Another example is subscribing to events:

useEffect(() => {
  function handleScroll() {
     setState(window.scrollY)
  }
  // document is an object in global scope, and will hold a reference
  // to the handleScroll function, preventing garbage collection
  document.addEventListener('scroll', handleScroll)
  // Uncomment the line below to avoid memory leak in your component
  // return () => document.removeEventListener(handleScroll)
}, [])

Another example worth remembering is the web API setInterval, which can also cause memory leak if you forget to call clearInterval when unmounting.

But that is not what I am doing, why should I care about this warning?

React's strategy to warn whenever state updates happen after your component has unmounted creates a lot of false positives. The most common I've seen is by setting state after an asynchronous network request:

async function handleSubmit() {
  setPending(true)
  await post('/someapi') // component might unmount while we're waiting
  setPending(false)
}

You could technically argue that this also is a memory leak, since the component isn't released immediately after it is no longer needed. If your "post" takes a long time to complete, then it will take a long time to for the memory to be released. However, this is not something you should worry about, because it will be garbage collected eventually. In these cases, you could simply ignore the warning.

But it is so annoying to see the warning, how do I remove it?

There are a lot of blogs and answers on stackoverflow suggesting to keep track of the mounted state of your component and wrap your state updates in an if-statement:

let isMountedRef = useRef(false)
useEffect(() => {
  isMountedRef.current = true
  return () => {
    isMountedRef.current = false
  }
}, [])

async function handleSubmit() {
  setPending(true)
  await post('/someapi')
  if (!isMountedRef.current) {
    setPending(false)
  }
}

This is not an recommended approach! Not only does it make the code less readable and adds runtime overhead, but it might also might not work well with future features of React. It also does nothing at all about the "memory leak", the component will still live just as long as without that extra code.

The recommended way to deal with this is to either cancel the asynchronous function (with for instance the AbortController API), or to ignore it.

In fact, React dev team recognises the fact that avoiding false positives is too difficult, and has removed the warning in v18 of React.

Maltase answered 27/2, 2022 at 16:28 Comment(1)
Also this approach is valid as long as those code block with hook is in calling them not either in a function component or a custom hook reactjs.org/warnings/invalid-hook-call-warning.htmlSwipple
E
8

You can try this set a state like this and check if your component mounted or not. This way you are sure that if your component is unmounted you are not trying to fetch something.

const [didMount, setDidMount] = useState(false); 

useEffect(() => {
   setDidMount(true);
   return () => setDidMount(false);
}, [])

if(!didMount) {
  return null;
}

return (
    <ArtistProfileContainer>
      <AlbumContainer>
        {artistData ? artistData.artistAlbums.items.map(album => {
          return (
            <AlbumTag
              image={album.images[0].url}
              name={album.name}
              artists={album.artists}
              key={album.id}
            />
          )
        })
        : null}
      </AlbumContainer>
    </ArtistProfileContainer>
  )

Hope this will help you.

Edgeways answered 2/3, 2019 at 15:50 Comment(6)
didMount will be true in the unmounted state.Floorer
Can you explain a bit further why?Edgeways
The component mounts, then the effect runs and sets didMount to true, then the component unmounts but didMount is never resetFloorer
I think you meant setDidMount(false). But the problem would be, the .then() handler closure will have a reference to an earlier didMountFloorer
This was a method that I solve an SSR issue in my app thought will go with this case as well. If not promise should be cancelled I guess.Edgeways
Error: Rendered more hooks than during the previous render.Magnificence
Z
5

I had a similar issue with a scroll to top and @CalosVallejo answer solved it :) Thank you so much!!

const ScrollToTop = () => { 

  const [showScroll, setShowScroll] = useState();

//------------------ solution
  useEffect(() => {
    checkScrollTop();
    return () => {
      setShowScroll({}); // This worked for me
    };
  }, []);
//-----------------  solution

  const checkScrollTop = () => {
    setShowScroll(true);
 
  };

  const scrollTop = () => {
    window.scrollTo({ top: 0, behavior: "smooth" });
 
  };

  window.addEventListener("scroll", checkScrollTop);

  return (
    <React.Fragment>
      <div className="back-to-top">
        <h1
          className="scrollTop"
          onClick={scrollTop}
          style={{ display: showScroll }}
        >
          {" "}
          Back to top <span>&#10230; </span>
        </h1>
      </div>
    </React.Fragment>
  );
};
Zalucki answered 8/3, 2021 at 6:13 Comment(1)
you have window.addEventListener("scroll", checkScrollTop); is renderParsec
C
4

I have getting same warning, This solution Worked for me ->

useEffect(() => {
    const unsubscribe = fetchData(); //subscribe
    return unsubscribe; //unsubscribe
}, []);

if you have more then one fetch function then

const getData = () => {
    fetch1();
    fetch2();
    fetch3();
}

useEffect(() => {
    const unsubscribe = getData(); //subscribe
    return unsubscribe; //unsubscribe
}, []);
Corby answered 4/5, 2021 at 13:31 Comment(0)
D
4

there are many answers but I thought I could demonstrate more simply how the abort works (at least how it fixed the issue for me):

useEffect(() => {
  // get abortion variables
  let abortController = new AbortController();
  let aborted = abortController.signal.aborted; // true || false
  async function fetchResults() {
    let response = await fetch(`[WEBSITE LINK]`);
    let data = await response.json();
    aborted = abortController.signal.aborted; // before 'if' statement check again if aborted
    if (aborted === false) {
      // All your 'set states' inside this kind of 'if' statement
      setState(data);
    }
  }
  fetchResults();
  return () => {
    abortController.abort();
  };
}, [])

Other Methods: https://medium.com/wesionary-team/how-to-fix-memory-leak-issue-in-react-js-using-hook-a5ecbf9becf8

Devastation answered 15/6, 2021 at 19:38 Comment(1)
This is a correct to verify aborted signalHeraldic
T
3

This error occurs when u perform state update on current component after navigating to other component:

for example

  axios
      .post(API.BASE_URI + API.LOGIN, { email: username, password: password })
      .then((res) => {
        if (res.status === 200) {
          dispatch(login(res.data.data)); // line#5 logging user in
          setSigningIn(false); // line#6 updating some state
        } else {
          setSigningIn(false);
          ToastAndroid.show(
            "Email or Password is not correct!",
            ToastAndroid.LONG
          );
        }
      })

In above case on line#5 I'm dispatching login action which in return navigates user to the dashboard and hence login screen now gets unmounted.
Now when React Native reaches as line#6 and see there is state being updated, it yells out loud that how do I do this, the login component is there no more.

Solution:

  axios
      .post(API.BASE_URI + API.LOGIN, { email: username, password: password })
      .then((res) => {
        if (res.status === 200) {
          setSigningIn(false); // line#6 updating some state -- moved this line up
          dispatch(login(res.data.data)); // line#5 logging user in
        } else {
          setSigningIn(false);
          ToastAndroid.show(
            "Email or Password is not correct!",
            ToastAndroid.LONG
          );
        }
      })

Just move react state update above, move line 6 up the line 5.
Now state is being updated before navigating the user away. WIN WIN

Transfusion answered 24/3, 2021 at 13:32 Comment(1)
Indeed this is the most case in RN, problem is if the redux store is separate from api fetchSwipple
U
0

If the user navigates away, or something else causes the component to get destroyed before the async call comes back and tries to setState on it, it will cause the error. It's generally harmless if it is, indeed, a late-finish async call. There's a couple of ways to silence the error.

If you're implementing a hook like useAsync you can declare your useStates with let instead of const, and, in the destructor returned by useEffect, set the setState function(s) to a no-op function.


export function useAsync<T, F extends IUseAsyncGettor<T>>(gettor: F, ...rest: Parameters<F>): IUseAsync<T> {
  let [parameters, setParameters] = useState(rest);
  if (parameters !== rest && parameters.some((_, i) => parameters[i] !== rest[i]))
    setParameters(rest);

  const refresh: () => void = useCallback(() => {
    const promise: Promise<T | void> = gettor
      .apply(null, parameters)
      .then(value => setTuple([value, { isLoading: false, promise, refresh, error: undefined }]))
      .catch(error => setTuple([undefined, { isLoading: false, promise, refresh, error }]));
    setTuple([undefined, { isLoading: true, promise, refresh, error: undefined }]);
    return promise;
  }, [gettor, parameters]);

  useEffect(() => {
    refresh();
    // and for when async finishes after user navs away //////////
    return () => { setTuple = setParameters = (() => undefined) } 
  }, [refresh]);

  let [tuple, setTuple] = useState<IUseAsync<T>>([undefined, { isLoading: true, refresh, promise: Promise.resolve() }]);
  return tuple;
}

That won't work well in a component, though. There, you can wrap useState in a function which tracks mounted/unmounted, and wraps the returned setState function with the if-check.

export const MyComponent = () => {
  const [numPendingPromises, setNumPendingPromises] = useUnlessUnmounted(useState(0));
  // ..etc.

// imported from elsewhere ////

export function useUnlessUnmounted<T>(useStateTuple: [val: T, setVal: Dispatch<SetStateAction<T>>]): [T, Dispatch<SetStateAction<T>>] {
  const [val, setVal] = useStateTuple;
  const [isMounted, setIsMounted] = useState(true);
  useEffect(() => () => setIsMounted(false), []);
  return [val, newVal => (isMounted ? setVal(newVal) : () => void 0)];
}

You could then create a useStateAsync hook to streamline a bit.

export function useStateAsync<T>(initialState: T | (() => T)): [T, Dispatch<SetStateAction<T>>] {
  return useUnlessUnmounted(useState(initialState));
}
Uncurl answered 2/4, 2021 at 1:45 Comment(0)
T
0

Try to add the dependencies in useEffect:

  useEffect(() => {
    fetchData()
    return () => { props.spotifyAPI.cancelRequest() }
  }, [fetchData, props.spotifyAPI])
Tengler answered 28/4, 2021 at 16:5 Comment(0)
Y
0

Usually this problem occurs when you showing the component conditionally, for example:

showModal && <Modal onClose={toggleModal}/> 

You can try to do some little tricks in the Modal onClose function, like

setTimeout(onClose, 0)
Yeanling answered 30/12, 2021 at 5:56 Comment(0)
M
0

This works for me :')

   const [state, setState] = useState({});
    useEffect( async ()=>{
          let data= await props.data; // data from API too
          setState(users);
        },[props.data]);
Mut answered 8/2, 2022 at 18:8 Comment(0)
Q
0

Similar problem with my app, I use a useEffect to fetch some data, and then update a state with that:

useEffect(() => {
  const fetchUser = async() => {
    const {
      data: {
        queryUser
      },
    } = await authFetch.get(`/auth/getUser?userId=${createdBy}`);

    setBlogUser(queryUser);
  };

  fetchUser();

  return () => {
    setBlogUser(null);
  };
}, [_id]);

This improves upon Carlos Vallejo's answer.

Quilmes answered 12/5, 2022 at 21:42 Comment(0)
V
0

I had this problem in React Native iOS and fixed it by moving my setState call into a catch. See below:

Bad code (caused the error):

  const signupHandler = async (email, password) => {
    setLoading(true)
    try {
      const token = await createUser(email, password)
      authContext.authenticate(token) 
    } catch (error) {
      Alert.alert('Error', 'Could not create user.')
    }
    setLoading(false) // this line was OUTSIDE the catch call and triggered an error!
  }

Good code (no error):

  const signupHandler = async (email, password) => {
    setLoading(true)
    try {
      const token = await createUser(email, password)
      authContext.authenticate(token) 
    } catch (error) {
      Alert.alert('Error', 'Could not create user.')
      setLoading(false) // moving this line INTO the catch call resolved the error!
    }
  }
Victual answered 16/5, 2022 at 21:57 Comment(0)
K
0
useEffect(() => {  
let abortController = new AbortController();  
// your async action is here  
return () => {  
abortController.abort();  
}  
}, []);

in the above code, I've used AbortController to unsubscribe the effect. When the a sync action is completed, then I abort the controller and unsubscribe the effect.

it work for me ....

Kibbutznik answered 9/7, 2022 at 5:34 Comment(0)
K
-1

The easy way

    let fetchingFunction= async()=>{
      // fetching
    }

React.useEffect(() => {
    fetchingFunction();
    return () => {
        fetchingFunction= null
    }
}, [])
Knotty answered 31/5, 2021 at 18:40 Comment(0)
C
-1
const onOk = useCallback(
    (moduleId?: number) => {
      /*
      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.
      at AddModal;
      */
      setTimeout(() => {
        setShowAddModal(false); 
      }, 10);
      getModuleList();
      const msg = moduleId;
      message.success(msg);
    },
    [getModuleList],
  );
Calcar answered 16/8, 2023 at 4:10 Comment(0)
E
-2
options={{
              filterType: "checkbox"
              ,
              textLabels: {
                body: {
                    noMatch:  isLoading ?
                    <CircularProgress />:
                        'Sorry, there is no matching data to display',
                },
            },
            }}
Eudoca answered 29/7, 2021 at 13:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.