Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

State updates might be asynchronous

What exactly do they mean? If I understand it correctly, I can't use this.state when calculating new state, unless I pass a function as a first parameter to setState():

// Wrong
this.setState({a: f(this.state)});

// Correct
this.setState(prevState => {return {a: f(prevState)}});

But I can use this.state to decide what to do:

if (this.state.a)
    this.setState({b: 2});

What about props?

// Correct or wrong?
this.setState({name: f(this.props)});

And supposedly I can't expect this.state to change after calling this.setState:

this.setState({a: 1});
console.log(this.state.a);   // not necessarily 1

Then, say I have a list of users. And a select where I can make one user current:

export default class App extends React.Component {
    ...

    setCurrentUserOption(option) {
        this.setState({currentUserOption: option});
        if (option)
            ls('currentUserOption', option);
        else
            ls.remove('currentUserOption');
    }

    handleAddUser(user) {
        const nUsers = this.state.users.length;
        this.setState(prevState => {
            return {users: prevState.users.concat(user)};
        }, () => {
            // here we might expect any number of users
            // but if first user was added, deleted and added again
            // two callbacks will be called and setCurrentUserOption
            // will eventually get passed a correct value

            // make first user added current
            if ( ! nUsers)
                this.setCurrentUserOption(this.userToOption(user));
        });
    }

    handleChangeUser(user) {
        this.setState(prevState => {
            return {users: prevState.users.map(u => u.id == user.id ? user : u)};
        }, () => {
            // again, we might expect any state here
            // but a sequence of callback will do the right thing
            // in the end

            // update value if current user was changed
            if (_.get(this.state, 'currentUserOption.value') == user.id)
                this.setCurrentUserOption(this.userToOption(user));
        });
    }

    handleDeleteUser(id) {
        this.setState(prevState => {
            return {users: _.reject(prevState.users, {id})};
        }, () => {
            // same here

            // choose first user if current one was deleted
            if (_.get(this.state, 'currentUserOption.value') == id)
                this.setCurrentUserOption(this.userToOption(this.state.users[0]));
        });
    }

    ...
}

Do all callbacks are executed in sequence after batch of changes to state was applied?

On second thought, setCurrentUserOption is basically like setState. It enqueues changes to this.state. Even if callbacks get called in sequence, I can't rely on this.state being changed by previous callback, can I? So it might be best not to extract setCurrentUserOption method:

handleAddUser(user) {
    const nUsers = this.state.users.length;
    this.setState(prevState => {
        let state = {users: prevState.users.concat(user)};
        if ( ! nUsers) {
            state['currentUserOption'] = this.userToOption(user);
            this.saveCurrentUserOption(state['currentUserOption']);
        }
        return state;
    });
}

saveCurrentUserOption(option) {
    if (option)
        ls('currentUserOption', option);
    else
        ls.remove('currentUserOption');
}

That way I get queuing of changes to currentUserOption for free.

like image 942
x-yuri Avatar asked Jul 21 '17 23:07

x-yuri


People also ask

Are React State updates synchronous?

React sets its state asynchronously because doing otherwise can result in an expensive operation. Making it synchronous might leave the browser unresponsive. Asynchronous setState calls are batched to provide a better user experience and performance.

Is setting state asynchronous?

To update the state of a component, you use the setState method. However it is easy to forget that the setState method is asynchronous, causing tricky to debug issues in your code.

How do you say that state updates are merged?

State Updates are MergedWhen you call setState() , React merges the object you provide into the current state . In the example below, we're updating the variable dogNeedsVaccination independently of the other state variables. The merging is shallow, so this.

Why should we not update the state directly?

When you directly update the state, it does not change this. state immediately. Instead, it creates a pending state transition, and accessing it after calling this method will only return the present value. You will lose control of the state across all components.


1 Answers

You didn't really ask a very specific question. "What does this mean" is not very much to go on. But you generally seem to understand the basics.

There are two possible ways to call setState(): either by passing it an object to get merged into the new state, or by passing it a function which returns an object which gets merged in a fashion similar to the first way.

So you either do this:

// Method #1
this.setState({foo: this.state.foo + 1}, this.someCallback);

Or this:

// Method #2
this.setState((prevState) => {return {foo: prevState.foo + 1}}, this.someCallback);

The main difference is that with method #1, foo will get incremented by 1 based on whatever state it was at the time you call setState(), while in method #2, foo will get incremented by 1 based on whatever the previous state was in the instant that the arrow function runs. So if you have multiple setState() calls that happen at the "same" time before the actual state update, with method #1 they may conflict and/or be based on outdated state, while with method #2 they are guaranteed to have the most up-to-date state because they update synchronously, one after another, in the state update phase.

Here is an illustrative example:


Method #1 JSBIN Example

// Method #1
class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {n: 0};
    this.increment.bind(this);
  }
  componentDidMount() {
    this.increment();
    this.increment();
    this.increment();
  }
  increment() {
    this.setState({n: this.state.n + 1}, () => {console.log(this.state.n)});
  }
  render() {
    return (      
      <h1>{this.state.n}</h1>      
    );
  }
}

React.render(
  <App />,
  document.getElementById('react_example')
);

In the above: you would see this in the console:

> 1
> 1
> 1

And the final value of this.state.n would be 1. All of the setState() calls were enqueued when the value of n was 0, so they're all simply set it to 0 + 1.


Method #2 JSBIN Example

// Method #2
class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {n: 0};
    this.increment.bind(this);
  }
  componentDidMount() {
    this.increment();
    this.increment();
    this.increment();
  }
  increment() {
    this.setState((prevState) => {return {n: prevState.n + 1}}, () => {console.log(this.state.n)});
  }
  render() {
    return (      
      <h1>{this.state.n}</h1>      
    );
  }
}

React.render(
  <App />,
  document.getElementById('react_example')
);

In the above, you would see this in the console:

> 3
> 3
> 3

And the final value of n would be 3. Like with method #1, all of the setState() calls were enqueued at the same time. However, since they use a function to synchronously update in order using the most current state - including changes to state made by concurrent state updates - they properly increment n three times as you would expect.


Now, why does console.log() show 3 three times for method #2, instead of 1, 2, 3? The answer is that setState() callbacks all happen together at the end of the state update phase in React, not immediately after that particular state update happens. So in that regard methods #1 and #2 are identical.

like image 92
jered Avatar answered Sep 20 '22 07:09

jered