简体   繁体   中英

ReactJS lifecycle setState in componentDidMount

I have 2 components for demonstration of my problem:

Parent:

import React from "react";
import ReactDOM from "react-dom";
import { Grid, Row } from "react-flexbox-grid";
import Hello from "./Hello";

class App extends React.Component {
  state = {
    name: "Michal"
  };

  componentDidMount = () => {
    this.setState({ name: "Tina" });
  };

  componentDidUpdate(prevState) {
    console.log("App componentDidUpdate", prevState, this.state);
  }

  handleUpdate = value => {
    console.log("App handleUpdate");
    this.setState({ name: value });
  };

  render() {
    return (
      <Grid>
        <Row>
          <Hello name={this.state.name} update={this.handleUpdate} />
        </Row>
      </Grid>
    );
  }
}

ReactDOM.render(<App />, document.getElementById("container"));

Child:

import * as React from "react";

class Hello extends React.PureComponent {
  componentDidMount() {
    // setTimeout(() => {
    this.props.update("Matus");
    // }, 0);
  }

  componentDidUpdate(prevProps) {
    console.log("Hello componentDidUpdate", prevProps, this.props);
  }

  render() {
    return <h1>Hello {this.props.name}!</h1>;
  }
}

export default Hello;

In child component I want to set value in parent state via props function. But setState function is ignored, it works if props function is called from setTimeout.

Can you explain me why it work in setTimeout, why I should avoid this construction. And what is correct way to do it?

Hello component represent "Select", which in componentDidMount will fetch options and set default value.

Thank you.

Components initialise from the bottom up in React. So in your example Hello triggers componentDidMount , attempts to set the state in App via this.props.update , then App overrides it a split-second later when it calls its own componentDidMount . The name you set in the child component never reaches the state.

I'm not sure what the purpose of this is, hopefully only for leaning purposes as components shouldn't need to immediately set their own state when mounting. If you need to perform some logic before initialising the state in App you can use a constructor and do it there.

Regardless, the solution is remove the initial state setter in App .

It is not ignored and it does fire. You are just not observing it with your logs. Check out:

https://codesandbox.io/s/kind-jackson-b2r2b?file=/src/App.js

In the console you will see the following execution order in the console window:

Hello componentDidMount props = Object {name: "Michal", update: function ()}
App handleUpdate value =  Matus 
App componentDidMount props = Object {}
Hello componentDidUpdate props = Object {name: "Tina", update: function ()}
App componentDidUpdate state = Object {}
Object {name: "Tina"}

Thus you will see the child componentDidMount fires and completes mount before the parent component completed and fires its componentDidMount , as components completes mounting from the child components up.

So you just never observe the state going to Matus because it triggers a new state change to Tina when it completes mounting.

You setState function from Hello component is ignored because of the React lifecycle. Basically App componentDidMount function overrides your state change from Hello component before it was rendered. That's why setTimeout helps, it moves your state change to the new rendering loop.

I don't know exact why you are trying to load data and pass it from the child component to parent but the good practice in React is to pass data from top to bottom. So the better solution would be to use Select component to just render the data from parent and react to user events.

<Select options={options} selected={option} handle={handleSelect} />

Reason:

  • React rendering is synchronous.
  • Rendering is a depth-first traversal

Now,

componentDidMount() {
      this.props.update("Matus");
    }

Is executed first, which sets the name Matus . Then the following executes -

componentDidMount = () => { this.setState({ name: "Tina" }); };

This sets the name Tina .

All of this happens on the first call-stack where the rendering happens. If we use setTimeout() , then

this.props.update("Matus");

will be moved to the second call-stack, which will be executed after the initial rendering and mounting has ended, thus setting the name Tina and triggering a re-render.

If you want to use class components, you need to use a constructor function to initialise state and pass the props from parent to child.

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      name: "Michal"
    };
  }

  // ... rest of parent component
import * as React from "react";

class Hello extends React.PureComponent {
  constructor(props) {
    super(props)
  }

  componentDidMount() {
    // setTimeout(() => {
    this.props.update("Matus");
    // }, 0);
  }

  componentDidUpdate(prevProps) {
    console.log("Hello componentDidUpdate", prevProps, this.props);
  }

  render() {
    return <h1>Hello {this.props.name}!</h1>;
  }
}

export default Hello;

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM