简体   繁体   中英

React updating state after component has been unmounted (memory leak)

I'm building a react app (am a noob at javascript) and struggling with react trying to update a state after the component has been unmounted.

Context for the react app: as part of a school project I have to build a movie ticket booking service for a small theatre complex, on the home page of the web app I have an image 'carousel' which is updating the image every 5 seconds.

Here is the home page in action: https://apollo-9dd16.web.app/

(the image loading time is just because I chose rather large images, will be fixed) - also the loading screen when first accessing the page is because it takes that long to load everything from firebase (everything is dynamically updated through firebase database) if there is a way to make it quicker please let me know

Also, I've gone through as many SO posts as I can find and everyone suggests adding a check if the component is mounted before performing state changes, which I think I've done?

Anyways, onto the problem, for some reason, it will come up saying I have a memory leak as I am trying to change the components state after it is unmounted (I can't reproduce it easily but it has happened a number of times)

import React, {Component} from 'react'
import { Link } from 'react-router-dom'
import MoviesContext from '../contexts/Movies'


/* Todo: 

 - Work on the transition between movies
 - Work on sending the user to the right place on book now button press

*/

class Home extends Component {
    static contextType = MoviesContext
    state = {
        intervalID:0,
        index:1,
        prevIndex:0,
        nextIndex:2,
        picList:[]
    }
    componentDidMount() {
        let mounted = true
        const posterURLS = [] // eslint-disable-next-line
        this.context.map((movies) => {
            posterURLS.push(movies.posterURL)
        })
        this.setState({picList: posterURLS})

        if(mounted) {
            const newIntervalId = setInterval(() => {
                if (this.state.nextIndex + 1 === this.state.picList.length ){
                    this.setState({ 
                        prevIndex: this.state.index,
                        index: this.state.nextIndex,
                        nextIndex: 0
                    })
                } else {
                    this.setState({
                        prevIndex: this.state.index,
                        index: this.state.nextIndex,
                        nextIndex: this.state.nextIndex + 1
                    })
                }
            }, 5000);
        
        
            this.setState(prevState => {
                return {
                    ...prevState,
                    intervalId: newIntervalId,
                };
            });

        }
        return () => mounted = false
    }
    render() {
        return (
            <div className="font-sans font-light text-theme-white text-4xl z-10 flex justify-center items-center h-screen">
                <div className="h-3/4">
                    <div className="h-full justify-center items-center">
                        <div className="h-full hidden md:flex">
                            <img src={this.state.picList[this.state.prevIndex]} alt="this is a movie" className="h-full rounded-2xl -mx-20"/>
                            <img src={this.state.picList[this.state.nextIndex]} alt="this is a movie" className="h-full rounded-2xl -mx-20"/>
                        </div>
                        <div className="absolute inset-10 flex justify-center items-center h-screen">
                            <img src={this.state.picList[this.state.index]} alt="this is a movie" className="h-3/4 rounded-2xl"/>
                        </div>
                    </div>
                    <div className="absolute inset-0 top-10 flex justify-center items-center">
                        <Link to="/book" className="bg-theme-light text-theme-black rounded-2xl py-3 px-5 hover:bg-theme-black hover:text-theme-light">Book Now</Link>
                    </div>
                </div>
                
            </div>  
        )  
    }

}

export default Home

You're pretty close: The reason that this is happening is because of this code:

/* 1 */  if (mounted) {
/* 2 */    const newIntervalId = setInterval(() => {
/* 3 */      if (this.state.nextIndex + 1 === this.state.picList.length) {
/* 4 */        this.setState({
/* 5 */          prevIndex: this.state.index,
/* 6 */          index: this.state.nextIndex,
/* 7 */          nextIndex: 0,
/* 8 */        });
/* 9 */      } else {
/* 10 */        this.setState({
/* 11 */          prevIndex: this.state.index,
/* 12 */          index: this.state.nextIndex,
/* 13 */          nextIndex: this.state.nextIndex + 1,
/* 14 */        });
/* 15 */      }
/* 16 */    }, 5000);
/* 17 */  
/* 18 */    this.setState((prevState) => {
/* 19 */      return {
/* 20 */        ...prevState,
/* 21 */        intervalId: newIntervalId,
/* 22 */      };
/* 23 */    });
/* 24 */  }
/* 25 */  

The if (mounted) condition on line 1 (in the above snippet) is up a bit too high. You should wrap the if statement around the setState calls on lines 4 and 10 as these are both inside the setInterval function callback.

Currently, the code is saying:

  • If the component is mounted
  • Register this interval which updates the state every 5 seconds

But what I believe you'd like your code to say:

  • If the component is mounted
  • Update the state every 5 seconds, but only if the component is still mounted
if (mounted) {
  const newIntervalId = setInterval(() => {
    if (this.state.nextIndex + 1 === this.state.picList.length) {
      if (mounted) {
        this.setState({
          prevIndex: this.state.index,
          index: this.state.nextIndex,
          nextIndex: 0,
        });
      }
    } else {
      if (mounted) {
        this.setState({
          prevIndex: this.state.index,
          index: this.state.nextIndex,
          nextIndex: this.state.nextIndex + 1,
        });
      }
    }
  }, 5000);

  this.setState((prevState) => {
    return {
      ...prevState,
      intervalId: newIntervalId,
    };
  });
}

That should fix the component update after the component is already unmounted. You might even be able to remove the if (mounted) check at the start of the above snippet.

The reason it's happening is that at the time the componentDidMount function is executed the mounted variable is true , so it'll register the setInterval callback and keeps executing it every 5 seconds.

While the mounted variable may no longer be true when the setInterval callback is executed, there's nothing stopping it from executing.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM