react: useEffect memory leak when setting state in fetch promise

Do you want to request a feature or report a bug? Reporting a possible bug

What is the current behavior? My app renders fine with no errors but I can’t seem to figure out why I keep getting this warning:

index.js:1446 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. in ArtistProfile (at App.js:51) in component (created by Route)

api-calls.js (Here’s a link): https://github.com/ryansaam/litphum/blob/master/src/api-calls.js

App.js

class App extends Component {
  constructor(props) {
    super(props)
    this.state = {
      user: {},
      spotifyAPI: {}
    }
  }
  componentDidMount() {
    if (user_token) {
      sessionStorage.setItem('access_token', user_token)
      this.setState({
        spotifyAPI: new spotifyAPI( user_token )
      })
    } else if (sessionStorage.getItem('access_token')) {
      this.setState({
        spotifyAPI: new spotifyAPI( sessionStorage.getItem('access_token') )
      })
    }
  }
  componentDidUpdate(prevProps, prevState) {
    if (this.state.spotifyAPI !== prevState.spotifyAPI)
      this.state.spotifyAPI.getUserProfile()
      .then(data => this.setState({user: data}))
  }
  
  render() {
    const { user, spotifyAPI } = this.state
    const token = sessionStorage.getItem('access_token')
    return (
      <Router>
        <div className="App">
            { (spotifyAPI.user_token && user)
            ? (<div className="logged-in">
                <div style={{width: "250px", height: "100%", position: "relative", float: "left"}} >
                  <Nav image={user.images ? user.images[0].url : null} user={user} />
                </div>
                <main id="main">
                  <Route path={`/${user.type}/${user.id}`} exact component={() => <Home spotifyAPI={spotifyAPI} />} />
                  <Route path="/artist/" component={() => <ArtistProfile spotifyAPI={spotifyAPI} />} />
                </main>
              </div>) 
            : <div onClick={() => window.location = "http://localhost:8888/login"} >log in</div>
            }
        </div>
      </Router>
    );
  }
}

ArtistProfile.js

const  ArtistProfile = props => {
  const [artistData, setArtistData] = useState(null)
  const { getArtist, getArtistAlbums, getArtistTopTracks } = props.spotifyAPI

  useEffect(() => {
    const id = window.location.pathname.split("/").pop()
    const ac = new AbortController()
    console.log(id)
    Promise.all([
      getArtist(id, ac),
      getArtistAlbums(id, ["album"],"US", 10, 0, ac),
      getArtistTopTracks(id, "US", ac)
    ])
    .then(response => {
      setArtistData({
        artist: response[0],
        artistAlbums: response[1],
        artistTopTracks: response[2]
      })
    })
    .catch(ex => console.error(ex))
    return () => ac.abort()
  }, [])
  console.log(artistData)
  return (
    <div>
      <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>
    </div>
  )
}

What is the expected behavior? If you can see in ArtistProfile.js I am using a clean up function that aborts when the component does unmount. The fetch would be aborted and state shouldn’t update but for some reason I am still getting this memory leak warning.

What I am expecting is for the warning to no longer throw because am using a clean up function that aborts the fetch.

Link to repo: https://github.com/ryansaam/litphum

My stackoverflow question: https://stackoverflow.com/questions/54954385/react-useeffect-causing-cant-perform-a-react-state-update-on-an-unmounted-comp/54964237#54964237

Which versions of React, and which browser React 16.8.2 Latest version of Chrome

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Comments: 31 (3 by maintainers)

Most upvoted comments

same question

image

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.

cancel promise solution


  const { adcode, regions = [] } = regionData;
  const [regionName, setRegionName] = useState('all');
  const [tableData, setTableData] = useState(regions);
  useEffect(() => {
    let isSubscribed = true;
    // getRegionName(adcode).then(setRegionName);
    getRegionName(adcode).then(() => {
      if(isSubscribed) {
        return setRegionName;
      }}
    );
    const promises = [];
    regions.forEach(item => {
      promises.push(
        getRegionName(item.code)
        .then(name => {item.name = name;})
        // eslint-disable-next-line no-console
        .catch(() => console.log('error', item.code)),
      );
    });
    Promise
    .all(promises)
    .then(() => {
      if(isSubscribed) {
        return setTableData(regions.filter(({ name }) => !!name));
      }
    });
    return () => isSubscribed = false;
  }, [adcode, regionData, regions]);

Let’s keep it open though. There might be a false positive warning in some cases.

It would really help if you turned this into a CodeSandbox…

To avoid calling setState after unmount from a hook, you can use the useEffect callback to set a ref value on unmount, and use that as a guard in your promise callbacks to check whether the component is still mounted before updating state. Here’s an example: https://github.com/ghengeveld/react-async/blob/master/src/useAsync.js

But to be honest you’re probably better off not reinventing this wheel and use useAsync directly, because it’s fully tested and covers other edge cases as well.

return () => {
            ignore = true;
        }

It is obvious that if this simple trick makes the difference it’s because React is bad designed. They cared so much about the management of the state that the state ended up managing them.

The solution to this problems it to use a flag to check if a component is unmounted, avoid setting state update, as the following in a useEffect hook.

        let ignore = false;
.
.
.
        if (!ignore)
            setServerError(errors);

        return () => {
            ignore = true;
        }

Yes but what if you’re dispatching actions to a context and want to dispatch even if the component unmounts ? For exemple you have a popin with a form that change the color of your website. You would still want to update the website color even if the user has closed the popin while the API call was still being processed

Well yeah this would work but that’s not the point, you could have a context that is not always mounted. The question being “What happens when react prints this warning”, is it harmful in some way ? If so how should we handle aisMounted boolean in the contexts ?

React 18 does not have this warning because it’s often pointless. (Such as in this case.)

I have the similar issue, I have canceled the axios request, but I get this Warning only on the first unmount of a component. This warning does not happen on the next umount of a component.

See below codes:

import React, { useEffect } from “react”; import CurrentOperatingStatus from “./current-operating-status” import { useDataApi } from ‘./lib/data-api’ import Controllers from ‘./cos-table’

export default function ContentOverall() { const [{ data, isLoading, isError, }, doFetch] = useDataApi( ‘/rest/machine-list’);

if (isError)
    return <div> Something went wrong...</div>;
else if (isLoading || data === null)
    return <div> Loading ...</div>;
else if (data && data.apiStatus.status == 'failure')
    throw new Error(response.data.apiStatus.message);

return (
    <div id="content" className="d-block col-auto FsUI_content">
        {/* content */}
        {/* content header */}
        <div className="w-100" id='1'>
            <div className="row FsUI_content_header">
                <div className="row d-xl-none w-100">
                    <ul className="nav FsUI_breadcrumbs">
                        <li>Overall Monitoring</li>
                    </ul>
                </div>
            </div>
        </div>
        <div className="row w-100 FsUI_block" id='2'>
            <CurrentOperatingStatus data={data?data.list:null} />
            <div className="col-auto FsUI_block">
                <div className="row align-items-end FsUI_block_header" id='1'>
                    <h6 className="FsUI_block_title">Current Operating Status List</h6>
                </div>
                <div className="FsUI_block_base" id='2'>
                    <Controllers data={data?data.list:null} />
                </div>
            </div>
        </div>
    </div>);

} ----data-api.js---- import { useState, useEffect } from ‘react’; import axios from “./axios”;

const useDataApi = (initialUrl) => { const [data, setData] = useState(null); const [url, setUrl] = useState(initialUrl); const [isLoading, setIsLoading] = useState(false); const [isError, setIsError] = useState(false); const CancelToken = axios.CancelToken; const source = CancelToken.source();

useEffect(() => {
    const fetchData = async () => {
        setIsError(false);
        setIsLoading(true);
        try {
            const result = await axios.get(url, {
                cancelToken: source.token
            });
            setData(result.data);
        } catch (error) {
            if (axios.isCancel(error)) {
                console.log('Request canceled', error.message);
            } else {
                // handle error
                setIsError(true);
            }
        }
        setIsLoading(false);
    };
    fetchData();

    return () => {
        source.cancel('Canceling HTTP request in useEffect cleanup function...');
    }
}, [url]);

return [{ data, isLoading, isError, source }, setUrl];

};

export { useDataApi };

Alright I will try my best to recreate it maybe using another API like a weather API. The only reason I couldn’t here was because I was using Spotify and you need to have an account to get an access token

Given that the latest release was 17.0.2 (March 22, 2021) and this warning was removed on Aug 18 https://github.com/facebook/react/pull/22114 – this issue is not going to be an issue someday based on that eh

I dont understand the solutions given here. Am also facing same issue

@TomPradat What’s harmful is the lack of one’s thought alignment with useEffect. The docs haven’t been updated since adaptation phase, so they still say it’s a cool replacement of lifecycle methods. It’s tricky understanding what’s under the hood just like flux was when it first came out, but you should get right to it instead of circling around the warning.

The solution to this problems it to use a flag to check if a component is unmounted, avoid setting state update, as the following in a useEffect hook.

        let ignore = false;
.
.
.
        if (!ignore)
            setServerError(errors);

        return () => {
            ignore = true;
        }

I tried to recreate the warning but I’m not getting it to show in this demo https://codesandbox.io/s/r5l297q3oq?fontsize=14