5

I am building a lazyloading component for images. But I have a problem with setting state. I'm getting Can only update a mounted or mounting component error, but I am using setState inside componentDidMount, which should allow me to avoid such errors.

Here's my code:

export default class Images extends React.Component {
    constructor(props) {
        super(props);

        this.element = null;

        this.state = {
            loaded: false,
            height: 0
        };
    }

    componentDidMount() {
        this.element = findDOMNode(this);

        this.loadImage();
    }

    getDimensions() {
        const w = this.element.offsetWidth;

        let initw = 0;
        let inith = 0;

        let result;

        const img = new Image();
        img.src = this.props.url;

        img.onload = (e) => {
            initw = e.path[0].width;
            inith = e.path[0].height;

            result = (w / initw) * inith;

            setTimeout(() => {
                this.setState({
                    loaded: true,
                    height: `${result}px`
                });
            });
        }
    }

    loadImage() {
        _scrolling.add([this.element], () => {
            if (this.element.classList.contains(_scrolling.classes.coming)) { // the image is visible
                this.getDimensions();
            }
        });
    }

    render() {
        const classes = this.state.loaded ? `${this.props.parentClass}__image--loaded` : null;
        const styles = this.state.loaded ? {
            maxHeight: this.state.height, minHeight: this.state.height, overflow: 'hidden'
        } : null;

        return (
            <div className={`${this.props.parentClass}__image ${classes}`} style={styles}>
                {this.state.loaded ?
                    <img
                        className={`${this.props.parentClass}__img`}
                        src={this.props.url}
                        title={this.props.title}
                        alt={this.props.title}
                    />
                    : null
                }
            </div>
        )
    }

I belive the problem lies within img.onload, but I don't know how to achieve this otherwise. What should I do?

Pardeep Dhingra
  • 3,746
  • 6
  • 27
  • 52
Tomek Buszewski
  • 6,696
  • 8
  • 58
  • 103

2 Answers2

4

If you attempt to set state on an unmounted component, you’ll get an error like that.There are two solutions:

  1. Assure Component isMounted : use setstate(); after checking that the component is mounted or not.

  2. Abort the Request: When the component unmounts, we can just throw away the request so the callback is never invoked. To do this, we’ll take advantage of another React lifecycle hook, componentWillUnmount.

Istiak Morsalin
  • 8,619
  • 6
  • 28
  • 56
2

It seems that the img.onload handler is getting called on an unmounted Images component instance.

Image loading is asynchronous and takes some time. When it’s finally done and img.onload handler gets called, there is no guarantee your component is still mounted.

You have to make use of componentWillUnmount and make sure you either:

  • Cancel the image loading before component gets unmounted, or
  • Keep the track of the component’s mounted state and check if it’s mounted once your handler gets called

More about checking if a component is mounted or not: https://facebook.github.io/react/blog/2015/12/16/ismounted-antipattern.html

Solution: Cancel The Image Loading

export default class Images extends React.Component {
    constructor(props) {
        super(props);

        this.element = null;

        this.state = {
            loaded: false,
            height: 0
        };

        this.images = []; // We’ll store references to the Image objects here.
    }

    componentDidMount() {
        this.element = findDOMNode(this);

        this.loadImage();
    }

    componentWillUnmount() {
        this.images.forEach(img => img.src = ''); // Cancel the loading of images.
    }

    getDimensions() {
        const w = this.element.offsetWidth;

        let initw = 0;
        let inith = 0;

        let result;

        const img = new Image();
        img.src = this.props.url;

        img.onload = (e) => {
            initw = e.path[0].width;
            inith = e.path[0].height;

            result = (w / initw) * inith;

            setTimeout(() => {
                this.setState({
                    loaded: true,
                    height: `${result}px`
                });
            });
        }

        this.images.push(img); // Store the reference.
    }

    loadImage() {
        _scrolling.add([this.element], () => {
            if (this.element.classList.contains(_scrolling.classes.coming)) { // the image is visible
                this.getDimensions();
            }
        });
    }

    render() {
        const classes = this.state.loaded ? `${this.props.parentClass}__image--loaded` : null;
        const styles = this.state.loaded ? {
            maxHeight: this.state.height, minHeight: this.state.height, overflow: 'hidden'
        } : null;

        return (
            <div className={`${this.props.parentClass}__image ${classes}`} style={styles}>
                {this.state.loaded ?
                    <img
                        className={`${this.props.parentClass}__img`}
                        src={this.props.url}
                        title={this.props.title}
                        alt={this.props.title}
                    />
                    : null
                }
            </div>
        )
    }
}

I copied the image cancelling from: https://stackoverflow.com/a/5278475/594458

Community
  • 1
  • 1
jokka
  • 1,275
  • 10
  • 10
  • It doesn't work without moving the single image to the other component. Additionally, it produces `Cannot read property 'getHostNode' of null` error. – Tomek Buszewski Aug 09 '16 at 08:55
  • Ok, I’ve updated the proposed solution. It stores the array of images and cancels them when the component is about to get unmounted. Hope it helps. – jokka Aug 09 '16 at 09:08
  • I tried the same solution and it also doesn't work. The problem comes when I rerender the parent copmonent (load more items via ajax). – Tomek Buszewski Aug 09 '16 at 09:12
  • Are you sure that the `setState` call in `onload` handler is the one raising the warning? Can you create a jsfiddle or codepen example? – jokka Aug 09 '16 at 09:28
  • Yes, I am sure, console traces it back to my js file and the line with `onload`. I can't create a fiddle for this, it's too complex. – Tomek Buszewski Aug 09 '16 at 09:29