7

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.

x-yuri
  • 16,722
  • 15
  • 114
  • 161
  • Just double checking are you using any state management frameworks like redux or flux? Answer could be different depending – aug Jul 21 '17 at 23:50
  • I'm going to. But for now I want to make it work without `redux`. – x-yuri Jul 22 '17 at 00:07

1 Answers1

26

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.

jered
  • 11,220
  • 2
  • 23
  • 34
  • After the component has mounted and is in DOM, increment() is invoked 3 times with each invoking the setState. Since each setState modifies the state, why isn't the render invoked 3 times. I see that it is invoked only a single time eventually. Please advise. – Farhan stands with Palestine Jul 02 '21 at 19:11
  • 2
    @FarhanShirgillAnsari `setState()` calls are queued and processed in "batches" as part of the React internal cycle. There is not a "render" after every single change to state, instead they are all processed together as much as possible. Multiple state updates may only result in a single re-render if they were triggered at the same time synchronously. – jered Jul 02 '21 at 19:50