score:15

Accepted answer

Why is using componentDidUpdate more recommended over the setState callback function?

1. Consistent logic

When using the callback argument to setState(), you might have two separate calls to setState() in different places which both update the same state, and you'd have to remember to use the same callback in both places.

A common example is making a call to a third-party service whenever a piece of state changes:

private method1(value) {
    this.setState({ value }, () => {
        SomeAPI.gotNewValue(this.state.value);
    });
}

private method2(newval) {
    this.setState({ value }); // forgot callback?
}

This is probably a logic error, since presumably you'd want to call the service any time the value changes.

This is why componentDidUpdate() is recommended:

public componentDidUpdate(prevProps, prevState) {
    if (this.state.value !== prevState.value) {
        SomeAPI.gotNewValue(this.state.value);
    }
}

private method1(value) {
    this.setState({ value });
}

private method2(newval) {
    this.setState({ value });
}

This way, the service is guaranteed to be called whenever the state updates.

Additionally, state could be updated from external code (e.g. Redux), and you won't have a chance to add a callback to those external updates.

2. Batched updates

The callback argument of setState() executes after the component is re-rendered. However, multiple calls to setState() are not guaranteed to cause multiple renders, due to batching.

Consider this component:

class Foo extends React.Component {
  constructor(props) {
    super(props);
    this.state = { value: 0 };
  }

  componentDidUpdate(prevProps, prevState) {
    console.log('componentDidUpdate: ' + this.state.value);
  }

  onClick = () => {
    this.setState(
      { value: 7 },
      () => console.log('onClick: ' + this.state.value));
    this.setState(
      { value: 42 },
      () => console.log('onClick: ' + this.state.value));
  }

  render() {
    return <button onClick={this.onClick}>{this.state.value}</button>;
  }
}

We have two setState() calls in the onClick() handler, each simply prints the new state value to the console.

You might expect onClick() to print the value 7 and then 42. But actually, it prints 42 twice! This is because the two setState() calls are batched together, and only cause one render to occur.

Also, we have a componentDidUpdate() which also prints the new value. Since we only have one render occurring, it is only executed once, and prints the value 42.

If you want consistency with batched updates, it's usually far easier to use componentDidMount().

2.1. When does batching occur?

It doesn't matter.

Batching is an optimization, and therefore you should never rely either on batching occurring or it not occurring. Future versions of React may perform more or less batching in different scenarios.

But, if you must know, in the current version of React (16.8.x), batching occurs in asynchronous user event handlers (e.g. onclick) and sometimes lifecycle methods if React has full control over the execution. All other contexts never use batching.

See this answer for more info: https://stackoverflow.com/a/48610973/640397

3. When is it better to use the setState callback?

When external code needs to wait for the state to be updated, you should use the setState callback instead of componentDidUpdate, and wrap it in a promise.

For example, suppose we have a Child component which looks like this:

interface IProps {
    onClick: () => Promise<void>;
}

class Child extends React.Component<IProps> {

    private async click() {
        await this.props.onClick();

        console.log('Parent notified of click');
    }

    render() {
        return <button onClick={this.click}>click me</button>;
    }
}

And we have a Parent component which must update some state when the child is clicked:

class Parent extends React.Component {
    constructor(props) {
        super(props);

        this.state = { clicked: false };
    }

    private setClicked = (): Promise<void> => {
        return new Promise((resolve) => this.setState({ clicked: true }, resolve));
    }

    render() {
        return <Child onClick={this.setClicked} />;
    }
}

In setClicked, we must create a Promise to return to the child, and the only way to do that is by passing a callback to setState.

It's not possible to create this Promise in componentDidUpdate, but even if it were, it wouldn't work properly due to batching.

Misc.

Since setState is asynchronous, I was thinking about using the setState callback function (2nd argument) to ensure that code is executed after state has been updated, similar to .then() for promises.

The callback for setState() doesn't quite work the same way as promises do, so it might be best to separate your knowledge.

Especially if I need a re-render in between subsequent setState calls.

Why would you ever need to re-render a component in between setState() calls?

The only reason I can imagine is if the parent component depends on some info from the child's DOM element, such as its width or height, and the parent sets some props on the child based on those values.

In your example, you call this.props.functionFromParentComponent(), which returns a value, which you then use to compute some state.

Firstly, derived state should be avoided, since memoization is a much better option. But even so, why not just have the parent pass the value directly down as a prop? Then you can at least compute the state value in getDerivedStateFromProps().

  //firstVariable may or may not have been changed, 
  //now someVariableBeforeSetStateCall may or may not get updated at the same time 
  //as firstVariableWasSet or firstVariable due to async nature of setState

These comments don't make much sense to me. The asynchronous nature of setState() doesn't imply anything about state not getting properly updated. The code should work as intended.


Related Query

More Query from same tag