Search code examples
javascriptreactjssetstateunmount

how can I setState in unmount Component in React


How can we setState in componentDidMount hook within an argument?

My problem is the warning of React notify that:

Warning: Can't call setState (or forceUpdate) 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 the 
componentWillUnmount method.
in DiscoverPage (created by Route)

In my DiscoverPage component:

import React, { Component } from 'react';
import { Switch, Route, Redirect } from 'react-router-dom';
import TvShows from './TvShows';
import Movies from './Movies';
import MovieDetail from "../MoviePage/MovieDetail";
import TvDetail from "../TvShowPage/TvDetail";

class DiscoverPage extends Component {

  constructor(props) {
    super(props);
    this.state = {
      data: {}
    }
  }

  getDataItem = (data) => {
    this.setState({ data: data });
  };

  render() {
    return (
      <Switch>
        <Route exact path="/discover/movie" render={props => <Movies data={this.getDataItem} {...props} />} />
        <Route path="/discover/tv" render={props => <TvShows data={this.getDataItem} {...props} />} />
        <Route path="/movie/:movie" render={props => <MovieDetail data={this.state.data} {...props} />} />
        <Route path="/tv/:tv" render={props => <TvDetail data={this.state.data} {...props} />} />
        <Redirect to="/discover/movie" />
      </Switch>
    );
  }
}

export default DiscoverPage;

In Child component (this case is Movies component):

import React, { Component } from 'react';
import requestApi from '../api';
import MovieList from "../MoviePage/MovieList";

class Movies extends Component {

  constructor(props) {
    super(props);
    this.state = {
      data: {
        page: 1,
        results: []
      }
    }
  }

  componentDidMount() {
    requestApi.fetchData('discover', 'movie').then(response => {
      this.setState({ data: response.data, isLoading: false });
    });
  }

  getMoviebyId = (id) => {
    requestApi.fetchDataById('movie', id).then(response => {
      this.props.data(response.data);
    });
  };

  nextPage = (e) => {
    const page = this.state.data.page + 1;
    requestApi.fetchDataPaginate('discover', 'movie', page).then(response => {
      this.setState({ data: response.data });
    });
  };
  prevPaginate = (e) => {
    const page = this.state.data.page - 1;
    requestApi.fetchDataPaginate('discover', 'movie', page).then(response => {
      this.setState({ data: response.data });
    });
  };

  render() {
    return (
      <div className="container">
        <div className="ss_media">
          <h2 className="title">Discover New Movies & TV Shows</h2>
          <MovieList
            movie={this.getMoviebyId}
            routeProps={this.props}
            prevPaginate={this.prevPaginate}
            nextPaginate={this.nextPage}
            moviesList={this.state.data} />
        </div>
      </div>
    );
  }
}

export default Movies;

Solution

  • The warning you're getting is telling you that you can't call setState on an unmounted component, i.e. a component that's not currently rendered. Most likely, one of your other components (Movies, TvShows, etc.) is calling the method you pass as the data prop. When one of these components is rendered, the DiscoveryPage component is not, so calling the getDataItem through the data prop will result in DiscoveryPage's setState method to be called, when it's not rendered. To fix this (assuming that what you want to do is pass data from component to component) I'd suggest maybe using localStorage to store a shared variable (or, as @Anu suggested, some other kind of store).

    I don't understand what you mean about "in arguments", but, answering your question, you can call setState in any component... method (even though calling it in componentWillUnmount makes little to no sense, since this methos is only called when the component is about to be unmounted).

    EDIT: Look at the Movie component. Using localStorage as our store, in the getMovieById method, instead of:

    getMoviebyId = (id) => {
        requestApi.fetchDataById('movie', id).then(response => {
            //this next sentence will call DiscoveryPage's setState
            //DiscoveryPage is not mounted, so a warning will be raised
            this.props.data(response.data);
        });
    };
    

    you could have something like:

    getMoviebyId = (id) => {
        requestApi.fetchDataById('movie', id).then(response => {
            //any component can access this store
            localStorage.setItem("data", response.data);
        });
    };
    

    and then, in some other component, or method, access it like so:

    localStorage.getItem("data");
    

    Your code is a little bit confusing. I'm having a little trouble understanding where you're using the DiscoveryPage's data, the one you're setting when you call the data method. But anyway, hope this helps.

    Just one more side note: When using localStorage, it's good practice to manage it, by which I mean calling localStorage.removeItem("data") when you're done using the data value. This is best done, for example, in some componentWillUnmount method.