1

I'm trying to learn ReactJS and found out about these lifecycles. However I've a doubt regarding how componentDidUpdate() functions and I want to know the reason behind it. Please take a look at the code below. It is a simple code that calculates the area of a triangle. Just to understand how we can send data from parent to child and vice versa I'm initialising the variables in parent component, then passing them to child to calculate the area and then passing back the results to change the state to final one where the results are rendered.

App.js

class App extends React.Component{
  constructor(props){
    super(props);
    console.log("In the parent's constructor");
    let initializeState=[{'base' : 0,'height' : 0,'area' : 0}];
    this.state={ values : initializeState }
    console.log(this.state.values);
  }

  componentDidMount(){
    console.log("Mounting values");
    let b = [{}];
    b['base']=10;
    b['height']=20;
    b['area']=0;
    this.setState({values: b});
  }

  update = (base,height,area) => {
    console.log("Updating state with calculated area");
    let updatedValue = [{}];
    updatedValue['base'] = base;
    updatedValue['height'] = height;
    updatedValue['area'] = area;
    this.setState({values: updatedValue});
  }

  render(){
    console.log('Inside Parent render');
    return(<React.Fragment>
      <Triangle val = {this.state.values} update={this.update} />
    </React.Fragment>
      )
  }
}

class Triangle extends React.Component{
  shouldComponentUpdate(newProps, newState){
    console.log('validating data');
    if(newProps.val.base >0 && newProps.val.height >0){
      return true;
    }else{
      return false;
    }
  }

  componentDidUpdate(prevProps, prevState, snapshot){
    console.log('In the child componentDidUpdate');
    console.log('If you\'ve reached this, state has been re-rendered')
    console.log(prevProps.val.base);
    console.log(prevProps.val.height);
    console.log(prevProps.val.area);
  }

  calcArea = () => {
    console.log('Calculating area now');
    let area = 1/2* this.props.val.base * this.props.val.height;
    this.props.update(this.props.val.base,this.props.val.height,area);
  }

  render(){
    console.log("In the child's render method")
    return(
    <React.Fragment>
      <h2>Base : {this.props.val.base}</h2>
      <h2>Height : {this.props.val.height}</h2>
      <h2>Area : {this.props.val.area} </h2>
      <button onClick={this.calcArea}>Click to calculate AREA</button>
    </React.Fragment>
    )
  }
}

export default App;

So Everything is working fine, i.e., This is the series of output:

In the parent's constructor
App.js:11 [{…}]
App.js:33 Inside Parent render
App.js:66 In the child's render method
App.js:15 Mounting values
App.js:33 Inside Parent render
App.js:43 validating data
App.js:66 In the child's render method
App.js:52 In the child componentDidUpdate
App.js:53 If you've reached this, state has been re-rendered

Now till this point, the component has re-rendered according to the new values mentioned inside componentDidMount function. However the next output is :

App.js:54 undefined
App.js:55 undefined
App.js:56 undefined

It should be the values that have been destroyed. ie., 0, 0, 0 (mentioned inside the parent's constructor using this.state ). Though when I hit the calculate Area and the component is re-rendered, it shows the correct values which have been destroyed. ie.,

Calculating area now
App.js:24 Updating state with calculated area
App.js:33 Inside Parent render
App.js:43 validating data
App.js:66 In the child's render method
App.js:52 In the child componentDidUpdate
App.js:53 If you've reached this, state has been re-rendered
App.js:54 10
App.js:55 20
App.js:56 0

Can someone tell me why the results are "undefined" when the state is being changed for the first time and not the values that we have instantiated ??

Divyam Dhadwal
  • 395
  • 3
  • 19
  • Congratulations on learning with such discipline! Let me ask some questions: In App, why are you setting a state value in the constructor and immediately after, in `componentDidMount`, changing it? Why does `calcArea` call `update` if the values of `base` and `height` didnt change? Why are you not updating the component if base or height are lower than 0? Why is the state in App instead of being in Triangle? – Alvaro Dec 16 '19 at 06:33
  • Well, I was just learning how the code execution works here. So I'm just putting some functionality in each of these methods so that I can see when they're called. calcArea is calling the update function because originally the area was 0. I wanted to calculate the area in the child and then pass it back to the parent so that the new state with the calculated area can be rendered. State I've mentioned in parent just because I wanted to see how i can use child to change it's state. This whole code is just meant to understand all these concepts. – Divyam Dhadwal Dec 16 '19 at 20:55

2 Answers2

1

There are some issues in the JavaScript which are probably the reason why you are getting undefined values.

let initializeState=[{'base' : 0,'height' : 0,'area' : 0}];
this.state={ values : initializeState }

Here you are setting the state as an object with a key values which then holds an array of objects [{}].

This leads to issues in other places where an array of objects is defined but then it is accessed like an object:

let b = [{}];
b['base']=10;

This code should be:

let b = {};
b.base = 10;

There is no need to use bracket notation when you are using a "normal" string as the key. This notation is used when using variables for the key or string keys that start with a number, have hyphens, etc:

const a_string = 'base';
const a_string_with_hyphen = 'some-key-name';

an_object[a_string] = 123;
an_object[a_string_with_hyphen] = 456;

Use let when defining a variable which value will change, otherwise use a const:

let value_that_will_change = 123;
const value_that_wont_change = 456;

value_that_will_change = 789;

Regarding react specifically, I changed the code a little to show different approaches, so that you can see how the state is changing. I used inputs to modify the values, that I think can be handy in this case:

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

        this.state = { base: 0, height: 0, area: 0 };

        console.log("App. constructor.", "state:", this.state);
    }

    componentDidMount() {
        const values = {
            base: 10,
            height: 20,
            area: 200
        };
        this.setState(values);

        console.log(
            "App. componentDidMount.",
            "state:",
            this.state,
            "Updating state with new values:",
            values
        );
    }

    componentDidUpdate(prevProps, prevState) {
        console.log(
            "App. componentDidUpdate.",
            "prev state:",
            prevState,
            "new state:",
            this.state
        );
    }

    updateBase = base_new => {
        this.setState({
            base: base_new,
            area: (1 / 2) * base_new * this.state.height
        });

        console.log("App. updateBase.", "new base:", base_new);
    };

    updateHeight = event => {
        const height_new = parseInt(event.target.value);

        this.setState({
            height: height_new,
            area: (1 / 2) * height_new * this.state.base
        });

        console.log("App. updateHeight.", "new height:", height_new);
    };

    doubleBase = () => {
        const { base, height } = this.state;
        const base_new = 2 * base;
        const area_new = (1 / 2) * base_new * height;

        this.setState({ base: base_new, area: area_new });

        console.log("App. doubleBase.", "new area:", area_new);
    };

    render() {
        const { state, updateBase, updateHeight, doubleBase } = this;

        console.log("App. render.", "state:", this.state);

        return (
            <Triangle
                {...state}
                updateBase={updateBase}
                updateHeight={updateHeight}
                doubleBase={doubleBase}
            />
        );
    }
}

class Triangle extends React.Component {
    componentDidMount() {
        const {
            updateBase,
            updateHeight,
            doubleBase,
            ...parent_state_props
        } = this.props;

        console.log("Triangle. componentDidMount.", "props:", parent_state_props);
    }

    componentDidUpdate(prevProps) {
        const {
            updateBase,
            updateHeight,
            doubleBase,
            ...parent_state_props
        } = this.props;
        const {
            updateBase: updateBase_prev,
            updateHeight: updateHeight_prev,
            doubleBase: doubleBase_prev,
            ...parent_state_props_prev
        } = prevProps;

        console.log(
            "Triangle. componentDidUpdate.",
            "prev props:",
            parent_state_props_prev,
            "new props:",
            parent_state_props
        );
    }

    render() {
        const {
            updateBase,
            updateHeight,
            doubleBase,
            ...parent_state_props
        } = this.props;
        const { base, height, area } = parent_state_props;

        console.log("Triangle. render.", "props:", parent_state_props);

        return (
            <React.Fragment>
                <label for="base" style={{ display: "block" }}>
                    Base
                </label>
                <input
                    id="base"
                    type="number"
                    value={base}
                    // Here we are defining the function directly and sending the value.
                    onChange={event => updateBase(parseInt(event.target.value))}
                />

                <label for="height" style={{ display: "block" }}>
                    Height
                </label>
                <input
                    id="height"
                    type="number"
                    value={height}
                    // Here we are sending the event to the function.
                    onChange={updateHeight}
                />

                <h2>{`Area: ${area}`}</h2>

                <button onClick={doubleBase}>Double base</button>
            </React.Fragment>
        );
    }
}

In the above code I left shouldComponentUpdate out. This method is used to prevent the Component from rendering. The reason for this is that every time a parent Component renders, it will make all its children render. This is ok if the props that the children receives changed, but not necessary when those received props didn't change. Basically nothing changed in the children but it is still rendering. If this supposes a performance issue you can use the PureComponent instead of Component, or use your own logic in shouldComponentUpdate.

One last thing, if you accept the advice, I encourage you to learn React Hooks which were introduced this year and is the new way to build with React.

Let me know if something is not clear or I missed something.

Alvaro
  • 9,247
  • 8
  • 49
  • 76
0

On componentDidMount in App component, we are setting the new values , previously they were undefined.

Now, inside componentDidUpdate in Triangle component, you are logging the prevProps which were never there, as a result they are undefined.

 componentDidUpdate(prevProps, prevState, snapshot){
      console.log('In the child componentDidUpdate');
      console.log('If you\'ve reached this, state has been re-rendered')
      console.log(prevProps.val.base);
      console.log(prevProps.val.height);
      console.log(prevProps.val.area);
      console.log(this.props.val.base);
      console.log(this.props.val.height);
      console.log(this.props.val.area);
    }

Change as above, you will get to know that the new props are set.

shubham
  • 414
  • 2
  • 3
  • there were no props initally when we mounted the child, as the parent itself did not have the values for base/height/area, Once the parent component mounted, we set the values in state of app which then passed down the values to triangle component, but componentDidUpdate takes in params the old params which were undefined as they were never there, you have received the new props in this.props and you are looking for them in prevProps – shubham Dec 16 '19 at 06:41
  • componentDidMount happens after the app component mounted, and here you are initializing the props to be passed, so when the component renders first time this values are undefined, and the child mounts with props as undefined. but then when we setState in componentDidMount in app.js then parent rerenders thus rerendering the child i.e triangle component – shubham Dec 16 '19 at 06:43
  • Hello. Thanks for answering. SO as you said that "On componentDidMount in App component, we are setting the new values , previously they were undefined. ". That is my question. Why are these undefined when I'm setting each of them to zero in the parent 's constructor using this.state ? – Divyam Dhadwal Dec 16 '19 at 20:51
  • put console.log("inital values props are : ", prevProps) inside `componentDidUpdate`, you will come to know what the previous values were clearly. – shubham Dec 17 '19 at 03:53
  • the way your passing values is not correct.... let initializeState=[{'base' : 0,'height' : 0,'area' : 0}]; should instead be let initializeState={'base' : 0,'height' : 0,'area' : 0}; this.state={ values : initializeState } – shubham Dec 17 '19 at 03:54