Paul Paul - 3 months ago 35
React JSX Question

React: Display loader whilst background image is loading results in setState error

I'm trying to create a component (within an isomorphic app) which loads a background image. Whilst the image is loading I want to display a spinner however I keep coming up with this error:

Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a no-op.


On init the loading state is set to false (so it doesn't show the spinner if js is disabled)

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


On componentDidMount, the loading state is set to
true
and an image is created using
props.src
and when loaded or errors calls a function.

componentDidMount = () => {
this.setState({loading: true});

this.image = new Image();
this.image.src = this.props.src;
this.image.onload = this.handleImageLoaded;
this.image.onerror = this.handleImageError;
}


Those functions will set the loading state to
false


handleImageLoaded = () => {
this.setState({loading: false});
}

handleImageError = () => {
this.setState({loading: false});
}


The render function will display a separate component
Loader
if state is true (this does render) and I will change the inline style background on img load.

return (
<div className={style.wrapper}>
<div className={style.background} style='background-image:'>
<Loader loading={this.state.loading} />
</div>
</div>
);

Answer

This is a pretty common problem, the error message tells you more or less what happened - you tried to call setState on an unmountedcomponent. In componentDidMount, you start loading an image and you set a callback (handleImageLoaded) to execute when the image loads (or errors). In this callback, you call setState. Since the loading is asynchronous, it can easily finish when the component is no longer in the document.

The way you handle this used to be to ask whether the component is still loaded before you modified the state:

handleImageLoaded = () => {
    if(this.isMounted())
       this.setState({loading: false});
}

Unfortunately, this will give you a warning now, saying that isMounted() is deprecated. What you're supposed to do now is essentially this:

{
    isStillMounted: true,
    componentWillUnmount: () => { this.isStillMounted = false; },
    handleImageLoaded: () => {
        if(this.isStillMounted)
            this.setState({loading: false});
    }
}

Ideally, you'd just cancel the loading in componentWillUnmount, but I don't think that's possible.

Also, like 1ven pointed out, if you're using native events rather than React events, it's a good idea to remove those handlers in componentWillUnmount, to prevent any potential memory leaks.