63
votes

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.

12
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 unmountedᆼᆺᆼ
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?Ryan Cogswell
@ııı How would I check if the component has unmounted?Ryan Sam

12 Answers

29
votes

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>
32
votes

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',
    })
}

9
votes

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(() => {
        setLoading(true);
        someResponse = await doVeryLongRequest(); // it needs 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}
            <a href="SOME_LOCAL_LINK">Go away from here!</a>
        </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 variable componentMounted (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({});
    let componentMounted = true; // (3) component is mounted
    // ...
    useEffect(() => {
        setLoading(true);
        someResponse = await doVeryLongRequest(); // it needs some time
        // When request is finished:
        if (componentMounted){ // (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 = false; // (4) set it to false if we leave the page
        }
    }, []);

    return (
        <div className={loading ? "loading" : ""}>
            {someData}
            <a href="SOME_LOCAL_LINK">Go away from here!</a>
        </div>
    );
}
7
votes

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.

2
votes

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>
  );
};
2
votes

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

0
votes

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));
}
0
votes

Try to add the dependencies in useEffect:

  useEffect(() => {
    fetchData()
    return () => { props.spotifyAPI.cancelRequest() }
  }, [fetchData, props.spotifyAPI])
0
votes

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
}, []);
0
votes

The easy way

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

React.useEffect(() => {
    fetchingFunction();
    return () => {
        fetchingFunction= null
    }
}, [])
0
votes

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

0
votes

options={{ filterType: "checkbox" , textLabels: { body: { noMatch: isLoading ? : 'Sorry, there is no matching data to display', }, }, }}