skip to Main Content

What is the correct place to load data in react?

According to the docs, the componentDidMount is the recommended place but it is run after rendering which appears to be a silly place to load data. Indeed my experiment below shows the data/state is never rendered. Only if data is loaded in componentWillMount do I see the results.

Example: In the following https://jsfiddle.net/sf9n7zj1/3/ the list of toDos is initially one item ['One'] and should subsequently load 2 more items to become ['One', 'Two', 'Three']. This happens correctly as is shown in the state but is never rendered and the UI only shows One.

todo list with one item only

Clicking on the "ToDos" <h2> tag in the fiddle will log the component state and there we see (in the developer console) that the 3 toDos and foo=1 are correctly set in state. Why is this never rendered?

console log

class TodoApp extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      foo: 0,
      toDos: ['One']
    }
    this.updateFoo = () => {
      console.log("updateFoo", this.state)
      this.setState((state) => {
        state.foo = 1;
      })
    }
  }
  // componentWillMount() {
  componentDidMount() {
    this.setState((state) => {
      state.toDos = [...state.toDos, 'Two', 'Three']
    })
    console.log("Loaded ToDos...", this.state);
  }
  
  render() {
    return (
      <div>
        <h2 onClick={this.updateFoo}>ToDos {this.state.foo}</h2>
        <ul>
          {this.state.toDos.map(item => <li key={item}>{item}</li>)}
        </ul>
      </div>
    )
  }
}

const container = document.getElementById('app');
const root = ReactDOM.createRoot(container);
root.render(<TodoApp />);

Note:

  • Moving the data loading to the (deprecated) componentWillMount causes everything to work but the react blog themselves recommend loading data in componentDidMount
  • Clicking sets state.foo to 1 but this too, is not rendered

2

Answers


  1. You’re not replacing state, you’re mutating state:

    state.toDos = [...state.toDos, 'Two', 'Three']
    

    So React doesn’t see that an update has been made. In the state setting operation, don’t change properties on the state object. Return a new object to reflect the updated state. For example:

    this.setState((state) => ({
      toDos: [...state.toDos, 'Two', 'Three']
    }))
    

    And:

    this.setState((state) => ({
      foo: 1
    }))
    

    Note the additional parentheses around the curly braces, indicating that this function is a single expression returning an object rather than a block of code.

    Login or Signup to reply.
  2. Your problem is a symptom of a different issue – state mutation.

    In both setState calls in the sample code you are mutating state state.toDos =. This will cause missed re-renders, which is why it appears that componentWillMount is working correctly but componentDidMount isn’t.

    Since componentWillMount runs before the first render, the state is "updated" (mutated) before it’s used the first time. With componentDidMount the state is updated after the render. This would normally just trigger a re-render and you’d see your correct value near instantly. But the mutation causes that re-render to be skipped.

    You can correct your state updates to be non-mutating with something like:

    this.setState((state) => ({
      toDos: [
        ...state.toDos, 
        'Two',
        'Three'
      ]
    }))
    

    Regarding but it is run after rendering which appears to be a silly place to load data, it may seem that way, but in reality the docs are correct. An additional render isn’t a problem, and most real world data loading scenarios are asynchronous, so regardless of the lifecycle method used to trigger it, it’s going to need to re-render.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search