Search code examples
reactjsreact-lifecycle

React - Multiple components did update - why?


Being new to React I'm trying to understand the Lifecycle hooks, is this case ComponentDidUpdate()

So I have a location class component - it's a simple form with an input for the user to type a location. My handleSubmit event prevents a new page request and instead updates the state in this higher order component.

Within location I have a weather class component that receives the new location as a prop, updates its own state with location and fires an api request on componentDidUpdate() which then also sets its state with the weather conditions for the location once the fetch is resolved. It then passes the state to a jsx div so the user can see the weather conditions.

For this, I can see three componentDidUpdate() events output to the console from the weather component - I'm only guessing what causes them.
The first when the new location prop is received from the higher location component?
The second on it setting it's own state to the new location?
The third when the fetch is resolved and it sets state with the weather?
Or perhaps one is when it updates the div with the weather conditions.

Please could you advise what is going on, it will really help me build a better app and debug it.

Thanks,

Phil

The Local component:

import React, { Component } from "react";
import './local.css';
import Earth from './Earth';
import { Weather } from '../spies/weather/Spy';

class Local extends Component {

    constructor() {
        super()
        this.state = {
            location:''
        }
    }

    handleSubmit = e => {
        e.preventDefault();
        if(e.target.elements.localInput.value) this.setState({ location: e.target.elements.localInput.value })        
        else return;
    }

    render() {
        return (
            <React.Fragment>
                <div id="local" >
                    <form className="appBorder" onSubmit={this.handleSubmit}>
                        <input id="localInput" className="appInput appBorder" type="text" placeholder="Enter the location">
                        </input>
                        <button className="appButton">
                            <Earth className="earth"/>
                        </button>
                    </form>
                </div>
                <Weather location={this.state.location}/>
            </React.Fragment>
        )
    }
}

export default Local;

The Weather component (alias Spy):

import React, { Component } from "react";
import './weather.css';
import '../spy.css';
import { getWeather } from './api';

class Spy extends Component {

    constructor() {
        super()
        this.state = {
            location: null,
            weatherData: null,
            error: '',
         };
    }

    componentDidUpdate(prevProps, prevState) {
        console.log("update");
        if (prevProps.location !== this.props.location) {
            this.setState({location: this.props.location},()=>{
                getWeather(this.state.location)
                    .then(data => {
                        console.log("api request resolved");
                        this.setState({ weatherData: data });
                    })
                    .catch(error => 
                        this.setState({ error: error.message }));
            }
        )}       
        else return;
    }

    render() {
        return (
            <div id="spyWeather" className="appBorder spy">
                <h3 className="spyName">Weather for {this.state.location}</h3> 
                <p id="weatherDesc" className="spyData">Conditions: {this.state.weatherData ? this.state.weatherData.current.weather_descriptions : ""}</p> 
                <p id="weatherTemp" className="spyData">Temperature: {this.state.weatherData ? this.state.weatherData.current.temperature : ""} &deg;C</p>
                <p id="weatherHumid" className="spyData">Humidity: {this.state.weatherData ? this.state.weatherData.current.humidity : ""} %</p>
                <p id="weatherPrecip" className="spyData">Precipitation: {this.state.weatherData ? this.state.weatherData.current.precip : ""} mm</p>
            </div>
        )
    }
}

export { Spy as Weather };

The console(2 updates, the api fire then another update):-

[HMR] Waiting for update signal from WDS...
2 Spy.js:18 update
Spy.js:23 api request resolved
Spy.js:18 update

Solution

  • Given

    componentDidUpdate(prevProps, prevState) {
        console.log("update");
        if (prevProps.location !== this.props.location) {
            this.setState({location: this.props.location},()=>{
                getWeather(this.state.location)
                    .then(data => {
                        console.log("api request resolved");
                        this.setState({ weatherData: data });
                    })
                    .catch(error => 
                        this.setState({ error: error.message }));
            }
        )}       
        else return;
    }
    

    And logs

    2 Spy.js:18 update
    Spy.js:23 api request resolved
    Spy.js:18 update
    

    Yes, I see three renders/rerenders.

    1. The first "update" is from when props.location changes from null to some value. The conditional test prevProps.location !== this.props.location resolves true, so the happy path is taken and state is updated with the location.

    2. The second "update" is now because state updated with location. At the same time from the previous state update, the setState callback has been invoked and weather fetched. In the happy path of the promise chain is the log "api request resolved" and another setState.

    3. The third "update" comes from state updating again, this time with weatherData.

    Just so you know, storing props in local state is an anti-pattern in react, simply issue your side-effects, like fetching weather when the props change. It is also not advisable to chain state updates in the setState callback as each nested state update delays that update 1 (or more) render cycle and can make debugging more difficult. It is better to simply handle them in componentDidUpdate. There is also no need for the "void" return as there is an implicit return from all JS functions that don't need to return any actual value.

    componentDidUpdate(prevProps, prevState) {
      console.log("update");
      if (prevProps.location !== this.props.location) {
        if (location) {
          // location prop changed and is truthy, get weather
          getWeather(this.state.location)
            .then(data => {
              console.log("api request resolved");
              this.setState({ weatherData: data });
            })
            .catch(error => this.setState({ error: error.message }));
        }     
      }
    }
    

    This should carve one of those initial "wasted" render cycles, the log should now be

    Spy.js:18 update
    Spy.js:23 api request resolved
    Spy.js:18 update