React setState Synchronization asynchronous problem


In function components, we use useState and useEffect most often. The setState update is asynchronous. Let’s first look at the setState asynchrony problem with the class component.

1. SetState () asynchrony problem in class components

import React, { PureComponent } from 'react';

class App extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      title: 'Hello Alice'}}changText() {
    this.setState({
      title: 'Hello, Li Yinhe'
    })
    console.log(this.state.title);  // Hello Alice
  }
  render() {
    return (
      <div>
        <h2>{this.state.title}</h2>
        <button onClick={e= >{this.changText()}}> Change the text</button>
      </div>); }}export default App;
Copy the code

We find that in changText(), we have setState to give the title a new value, but console.log() prints the same value as the original state.

So setState is asynchronous, and we can’t get the latest result immediately after setState

2. Why make setState asynchronous?

I believe you will have such a question when you first touch setState. Isn’t it good to directly synchronize updates?

  • Then I went to find a lot of materials, which were not very clear. Finally, I saw someone discussing this problem on Github, and this problem
  • React core member (and Redux writer) Dan Abramov also wrote a lengthy response explaining why
  • Github.com/facebook/re…

Several things can be summarized from Dan Abramov’s response

  • setStateDesigned as asynchronous, it can significantly improve performance;
    • If setState is updated every time, it means that the render function will be called frequently and the interface will be re-rendered, so the efficiency of synchronous update is very low.
    • If we know we’re going to get multiple updates, it’s better to do them in batches.
  • If the synchronization updates state, but the render function has not been executed, then state and props cannot be kept in sync;
    • The inconsistency between the props and the state can cause a lot of problems in development.

So how do you get the updated values in the class component?

  • SetState () takes two arguments: the second argument is a callback that will be executed after the update;
  • The format is as follows:setState(partialState, callback)

Let’s modify the above code and see that the latest result is available via the callback function

changeText() {
  this.setState({
    message: "Hello, Li Yinhe."
  }, () = > {
    console.log(this.state.message); // Hello, Li Yinhe
  });
}
Copy the code

Of course, we can also implement life cycle functions:

componentDidUpdate(prevProps, provState, snapshot) {
  console.log(this.state.message);
}
Copy the code

3. Does setState have to be asynchronously updated?

No, we can do the following two tests

1. Use setTimeOut

changeText() {
  setTimeout(() = > {
    this.setState({
      message: "Hello, Li Yinhe."
    });
    console.log(this.state.message); // Hello, Li Yinhe
  }, 0);
}
Copy the code

2. Use native DOM events

componentDidMount() {
  const btnEl = document.getElementById("btn");
  btnEl.addEventListener('click'.() = > {
    this.setState({
      message: "Hello, Li Yinhe."
    });
    console.log(this.state.message); // Hello, Li Yinhe})}Copy the code

It can be divided into two cases:

  • In the component life cycle or React composite event, setState is asynchronous;
  • In setTimeout or native DOM events, setState is synchronous;

4. Functional components through hook,setState() async problem

Let’s look at the example I did today

import React, { useState, useEffect } from 'react';
import { layoutEmitter } from '@/utils/EventEmitter';

interface ItemProps {
    initNumber: number;
}
export default ({ initNumber = 0 }: ItemProps) => {
    const [state, setState] = useState(initNumber);
    
    return <button
        style={{ fontSize: '30px'}}onClick={()= > {
            setState(state => state + 1)
            layoutEmitter.emit({ state });
        }}
    >EventEmitter {state} </button>
};
Copy the code

Inside button’s onClick event, setState is passed to LayoutEmitters ({state}). SetState in useState has only one parameter, unlike setState in the class component. The second parameter is a callback function, which can achieve synchronous update in the callback function

5. Synchronous update of functional components

So how do we implement synchronous updates? Passing a callback function in the second argument to setState()?

It didn’t work and a warning was issued

Warning: State updates from the useState() and useReducer() Hooks don't support the second callback argument. 
Copy the code

Since useState Hook doesn’t work, let’s try another Hook. Since React is now fully embracing Hook, it must have its powerful place

6. Use useEffect to synchronize and update data

I checked github again to see if there was any question about me. Finally, I saw a reply from Dan Abramov in a discussion, suggesting that we useEffect

Github.com/facebook/re…

We learned about useEffect earlier, and its first argument is a callback function, which by default is executed for every update

So we can setState() when the button is clicked and then pass the parameter in userEffect so that the data we get is up to date, so let’s try that

import React, { useState, useEffect } from 'react';
import { layoutEmitter } from '@/utils/EventEmitter';

interface ItemProps {
    initNumber: number;
}
export default ({ initNumber = 0 }: ItemProps) => {

    const [state, setState] = useState(initNumber);

    useEffect(() = > {
        layoutEmitter.emit({ state });
    }, [state])

    return <button
        style={{ fontSize: '30px'}}onClick={()= > {
            setState(state => state + 1)
        }}
    >EventEmitter {state} </button>
};
Copy the code

So once the code is written, let’s run it and see what’s wrong

PLAINTEXT
Type error: _this.subscriptions not a function 
Copy the code

We looked at EventEmitter. Ts and index_ts and found that the code was executed in the order of calling useSubscription in index_ts and adding this. The emit is then called through this

type Subscription<T> = (val: T) = > void;
class EventEmitter<T> {
    private subscriptions: { (arg0: T): void; (val: T): void; } | undefined;

    emit = (val: T) = > {
        //@ts-ignore
        this.subscriptions(val);
    };

    useSubscription = (callback: Subscription<T>) = > {
        function subscription(val: T) {
            if(callback) { callback(val); }}this.subscriptions = subscription;
    };
}
const layoutEmitter = new EventEmitter();
export { layoutEmitter };
Copy the code

Our index-ts uses this component, which causes useEffect to be executed when the index is attached to the DOM. Layoutemitters ({state}) are called once before the button is clicked.

This. Subscriptions = subscription; It hasn’t been implemented yet

export default() = > {const [list, setList] = useState([]);

    useEffect(() = > {
        layoutEmitter.useSubscription((data) = > {
            list.push(data);
            console.log(data);
            console.log(list);
            const listData = [...list]
            setList(listData);
        });
    }, [])

    return (
        <div>
            <EventEmitterButton initNumber={11} />
            <p>list length:{list.length}</p>
            {
                list.map((item: ItemProps) => <p key={item.state}>{item.state}</p>)}</div>)};Copy the code

So how do we solve this problem?

The second argument to useEffect, an array of DependencyList, helps us solve this problem.

This parameter is used to re-execute the useEffect only when the state changes. (Influenced by whom)

Let’s take a look at the case:

//1. The second parameter is left blank. DependencyList is an optional parameter, it doesn't matter if we don't write it
	  useEffect(() = >{
    console.log(props.number)
    setNumber(props.number)
  }) // All updates are executed
  
//2. An empty array
  useEffect(() = >{
    console.log(props)
  },[]) // Execute only during mount and unmount

//3. Define state
  const [state,setState] = useState(0)
  useEffect(() = >{
    console.log(state)
  },[state]) //count is executed when updated
Copy the code

With that in mind, let’s move on to refining our code

export default ({ initNumber = 0 }: ItemProps) => {
    const [state, setState] = useState(initNumber);
    useEffect(() = > {
        layoutEmitter.emit({ state });
    }, [state])
    return <button
        style={{ fontSize: '30px'}}onClick={()= > {
            setState(state => state + 1)
            setFlag(false)
        }}
    >EventEmitter {state} </button>
};
Copy the code

If the index is attached to the Dom, the DependencyList will be executed, and then an initNumber will be passed.

Const [flag, setFlag] = useState(true); To make sure that useEffect doesn’t fire when we don’t click the button, and then put the condition inside useEffect so that we can do that

Ps: useEffect cannot be placed in a conditional statement, which is the specification of Hook// 1. InitNumber specifies the parameter type
export default ({ initNumber = 0 }: ItemProps) => {
    const [state, setState] = useState(initNumber);
    const [flag, setFlag] = useState(true);
    useEffect(() = > {
        if(! flag) { layoutEmitter.emit({ state }); } }, [state])return <button
        style={{ fontSize: '30px'}}onClick={()= > {
            setState(state => state + 1)
            setFlag(false)
        }}
    >EventEmitter {state} </button>
};
Copy the code

This will allow us to fulfill our requirements and render the correct data on the page when the button is clicked.

The above implementation due to my current technical shortage, a new way, this implementation may bring some problems.

6. Discuss React setState

Is setState a macro task or a micro task in React? One comment below the article reads:

In layman’s terms, setState is a pseudo-asynchronous, or what you can call defer, which is deferred but still in an event loop itself, so it executes after synchronous code and before asynchronous code. Why does this happen? React batch updates also benefit from compositing events. Try getting rid of the React event and using setState with the native event. You’ll get synchronized code.