Search code examples
javascriptreactjsjsxrenderlifecycle

React component render after event listener


I'm having some trouble getting component lifecycles to cooperate with event listeners in React. Below is an example application that displays an image from a URL provided by a user and uses the width of the image to apply styling - in this case I want the image to appear at 1/2 scale so i get the width of the image and then give it a width of 1/2 it's actual width.

To do this I use the naturalWidth of the image as a property in the render() method. I create an instance of the image in the componentDidMount() method and then use an event listener to wait until the image has loaded to get its width. Without the event listener it will try to grab the width before the image is loaded and won't return a value so the listener ensures that the image is loaded before we get the width. That's where I'm running into a problem: The render() method is not restricted by the event listener so even though componentDidMount() is triggered before render(), it always renders with the initial state. I was hoping that by changing the state as part of the event the component would re-render but that's not the case.

What is the "right" way to make sure the content renders AFTER the event listener has finished getting the value for the width of the image?

import React, {Component,} from 'react'
import './app.css'

class App extends Component {

// setting the initial state with a placeholder image
state = {
    src: "https://cdn.jpegmini.com/user/images/slider_puffin_jpegmini_mobile.jpg",
    width: "",
}

// Getting the width of the image when the component mounts
componentDidMount() {
    let Img = document.createElement("img");
    Img.src = this.state.src
    Img.addEventListener("load", () => {
        this.state.width = Img.naturalWidth
    });
}

//re-calculating the width when the image changes (exact same as above)
componentDidUpdate() {
    let Img = document.createElement("img");
    Img.src = this.state.src
    Img.addEventListener("load", () => {
        this.state.width = Img.naturalWidth
    });
}

// This function updates the state when the input is changed
change = (e) => {
    this.setState({
  [e.target.name]: e.target.value
    })
};

render() {

    return (
        <div className="app">
                <input
                    name="src"
                    value={this.state.src}
                    onChange={e => this.change(e)}
                    disabled={false}
                    ></input>
                <img
                    src={this.state.src}
                    style={{width: this.state.width / 2}}
                    ></img>
            </div>
    )

}
}

export default App

Solution

  • You can conditionally render the image and only display it if this.state.width holds a value.

    Also, you should use setState, rather than mutate the existing state (which is not the React way of doing things, and won't result in a state update).

    Rather than repeating the image width calculation twice, put it into a function and call that function twice. Also remember to use const instead of let whenever possible.

    calcImageWidth() {
        const img = document.createElement("img");
        img.src = this.state.src
        img.addEventListener("load", () => {
            // use this technique in componentDidUpdate too
            this.setState(prevState => ({ ...prevState, width: img.naturalWidth }));
        });
    }
    componentDidMount() {
      this.calcImageWidth();
    }
    componentDidUpdate() {
      this.calcImageWidth();
    }
    
    render() {
        return (
            <div className="app">
                <input
                    name="src"
                    value={this.state.src}
                    onChange={e => this.change(e)}
                    disabled={false}
                ></input>
                {
                    this.state.width === ''
                        ? null
                        : <img
                              src={this.state.src}
                              style={{ width: this.state.width / 2 }}
                          ></img>
                }
            </div>
        );
    }