background

It all started because of the following code, and even discussed with my friends for a long time. You can take a look first, and we will talk about it in detail later, and poke 👉 codesandbox

import React, { useState, useEffect } from 'react'

function Article({ id }) {
  const [article, setArticle] = useState(null)
  
  useEffect(() = > {
    let didCancel = false
    console.log('effect', didCancel)

    async function fetchData() {
      console.log('setArticle begin', didCancel)
      new Promise((resolve) = > {
        setTimeout(() = > {
          resolve(id)
        }, id);
      }).then(article= > {
        // Quickly click the button on Add ID. Why is true printed here
        console.log('setArticle end', didCancel, article)
        // if (! DidCancel) {// Commenting out this line of code causes an error overwriting the status value
          setArticle(article)
        // }})}console.log('fetchData begin', didCancel)
    fetchData()
    console.log('fetchData end', didCancel)

    return () = > {
      didCancel = true
      console.log('clear', didCancel)
    }

  }, [id])

  return <div>{article}</div>
}

function App() {
  const [id, setId] = useState(5000)
  function handleClick() {
    setId(id-1000)}return (
    <>
      <button onClick={handleClick}>add id</button>
      <Article id={id}/>
    </>
  );
}

export default App;
Copy the code

The key code in useEffect is to modify the value of didCancel by clearing the side effect function, and then determine whether to perform setState immediately based on the value of didCancel, essentially to resolve the race situation.

A race is an error overwriting the state value in code that is mixed async/await and top-down data flow (the props and state may change during an async function call)

For example, in the above example, after we quickly click the button twice, we will see 3000 first and then see the result of 4000 on the page. This is because the state of 4000 is executed first, but returned later, so it will overwrite the last state, so we finally see 4000

UseEffect Clears the side effect function

We know that if we return a function in useEffect, that function is the cleanup function, and it is executed when the component is destroyed, but in fact, it is executed every time the component is re-rendered, removing the side effects of the previous effect.

A side effect is when a function does something that has nothing to do with the return value of its operation, such as changing a global variable, changing an argument passed in, or even console.log(), so Ajax operations, DOM changes, timers, and other asynchronous operations are all side effects

Consider the following code:

useEffect(() = > {
  ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange);
  return () = > {
    ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange);
  };
});
Copy the code

If the first render is {id: 10} and the second render is {id: 20}. Here’s what you might think happened:

  • The React to clear the{id: 10}The effect of
  • The React to render{id: 20}The UI
  • Run the React{id: 20}The effect of

(It doesn’t.)

React will only run Effects after the browser has drawn. This makes your application much smoother because most effects don’t block updates on the screen. Effect clearing is also delayed, and the previous Effect will be cleared after rerendering:

  • The React to render{id: 20}The UI
  • Browser drawn, seen on the screen{id: 20}The UI
  • The React to clear{id: 10}The effect of
  • Run the React{id: 20}The effect of

If clearing the previous effect happened after the props became {id: 20}, why did it still get the old {id: 10}?

Because each function within the component (including event handlers, effects, timers, or API calls, and so on) captures props and state in the render in which they were defined

So, an effect clearance does not read the latest props, it only reads the props value in the render in which it was defined

Analysis of the original 🌰

Analysis of the

Going back to our original example, let go of the uncommented code, and you have the following analysis.

After the first rendering

function Article() {... useEffect(() = > {
    let didCancel = false
    async function fetchData() {
      new Promise((resolve) = > {
        setTimeout(() = > {
          resolve(id)
        }, id);
      }).then(article= > {
        if(! didCancel) { setArticle(article) } }) } fetchData() }, [5000])
  return () = > {
    // Clear this render side effect, number it NO1, there is a hidden message, in this function, didCancel = false before execution
    didCancel = true}}// Wait 5s, the page displays 5000,
Copy the code

By clicking a breakpoint on console.log(‘setArticle end’, didCancel, article), we can visually analyze the next operation 👉 by clicking the button twice quickly

/** On the first click, after the page is drawn, useEffect first executes the previous cleanup function, NO1, which sets didCancel in the last effect closure to true */
function Article() {... useEffect(() = > {
    let didCancel = false
    async function fetchData() {
      new Promise((resolve) = > {
        setTimeout(() = > { // setTimeout1
          resolve(id)
        }, id);
      }).then(article= > {
        if(! didCancel) { setArticle(article) } }) } fetchData() }, [4000])
  return () = > {
    // Clear this render side effect, number it NO2, there is a hidden message, didCancel = false in scope of this function
    didCancel = true}}Copy the code

As you can see from DevTools:

/** Second click, after the page is drawn, useEffect first executes the previous cleanup function, NO2, which sets didCancel in the last effect closure to true */
function Article() {... useEffect(() = > {
    let didCancel = false
    async function fetchData() {
      new Promise((resolve) = > {
        setTimeout(() = > { // setTimeout2
          resolve(id)
        }, id);
      }).then(article= > {
        if(! didCancel) { setArticle(article) } }) } fetchData() }, [3000])
  return () = > {
    // Clear this render side effect, number it NO3, there is a hidden message, didCancel = false in scope of this function
    didCancel = true}}Copy the code

As you can see from DevTools:

conclusion

After the second click, setTimeout2 is done first, didCancel is false, so setArticle is done, and the page shows 3000, why is didCancel false here, because NO2 is not doing clear, It is executed the next time the component is re-rendered, or when the component is unloaded.

After about 1s more, setTimeout2 completes, at which point didCancel is set to true by NO2’s clear function, so it will not perform the setArticle operation. So you don’t see 4000 and then 3000.

UseEffect Specifies how data is requested

Get data with async/await

// If you want to request initialization data while the component is hanging, you might use the following syntax
function App() {
    const [data, setData] = useState()
    useEffect(async() = > {const result = await axios('/api/getData')
        
        setData(result.data)
    })
}
Copy the code

However, we will notice that there is a warning message in the console:

We cannot use async directly in useEffect because the async function declaration defines an asynchronous function that returns an implicit Promise by default. However, in Effect Hook we should return nothing or a clear function. So we could do the following

function App() {
    const [data, setData] = useState()
    useEffect(() = > {
        const fetchData = async() = > {const result = await axios(
            '/api/getData',); setData(result.data); }; fetchData(); })}Copy the code

Tell React exactly what your dependencies are

function Greeting({ name }) {
  const [counter, setCounter] = useState(0);

  useEffect(() = > {
    document.title = 'Hello, ' + name;
  });

  return (
    <h1 className="Greeting">
      Hello, {name}
      <button onClick={()= > setCounter(counter + 1)}>Increment</button>
    </h1>
  );
}
Copy the code

Effect hook will be executed every time we click on button to make counter+1. This is not necessary. We can add name to the dependency array of Effect, which tells React that when my name changes, You help me execute the function in effect.

If we add values from all of the components used in effects to our dependencies, sometimes the results are not very good. Such as:

useEffect(() = > {
    const id = setInterval(() = > {
        setCount(count+1)},1000)
    return () = > clearInterval(id)
}, [count])
Copy the code

Although effect execution is triggered each time the count changes, the timer is recreated each time it is executed, which is not optimal. We added the count dependency because we use count in the setCount call, but we don’t use count anywhere else, so we can change the setCount call to a function form, so that setCount gets the current count value every time the timer is updated. So in the effect dependent array, we can kick count

useEffect(() = > {
    const id = setInterval(() = > {
        setCount(count= > count+1)},1000)
    return () = > clearInterval(id)
}, [])
Copy the code

Decouple updates from Actions

Let’s modify the above example to include two states: count and step

function Counter() {
  const [count, setCount] = useState(0);
  const [step, setStep] = useState(1);

  useEffect(() = > {
    const id = setInterval(() = > {
      setCount(c= > c + step);    
    }, 1000);
    return () = > clearInterval(id);
  }, [step]);
  return (
    <>
      <h1>{count}</h1>
      <input value={step} onChange={e= > setStep(Number(e.target.value))} />
    </>
  );
}
Copy the code

At this point, modifying step will restart the timer because it is one of the dependencies. If we don’t want to restart the timer after a step change, how can we remove the dependency on step from Effect?

When you want to update a state that depends on another state, in this case count depends on step, you can replace them with useReducer

function Counter() {
  const [state, dispatch] = useReducer(reducer, initState)
  const { count, step } = state
  
  const initState = {
      count: 0.step: 1
  }
  
  function reducer(state, action) {
      const { count, step } = state
      switch (action.type) {
          case 'tick':
              return { count: count + step, step }
          case 'step':
              return { count, step: action.step }
          default:
              throw new Error()
      }
  }

  useEffect(() = > {
    const id = setInterval(() = > {
      dispatch({ type: 'tick'})},1000);
    return () = > clearInterval(id);
  }, [dispatch]);
  
  return (
    <>
      <h1>{count}</h1>
      <input value={step} onChange={e= > setStep(Number(e.target.value))} />
    </>
  );
}
Copy the code

Making Dispatch an Effect dependency in the code above will not trigger the execution of effect every time, because React guarantees that the Dispatch will remain the same for the duration of the component’s declaration cycle, so the timer will not be recreated.

You can remove the dispatch, setState, and useRef package values from dependencies because React ensures that they are static

Instead of reading the state directly in effect, it dispatches an action to describe what happened, which decouples our effect and step states. Our Effect no longer cares about updating the state, it just tells us what’s going on. All the updated logic was sent to reducer for unified processing

When you dispatch, React just remembers the action, and it will call the Reducer again on the next render, so the Reducer has access to the latest props in the component

conclusion

The purpose of this article is to help you re-understand useEffect and pay attention to the points of requesting data in useEffect. Please refer to 👇 for more details. If there are any errors in the above content, please feel free to point out.

Refer to the link

Overreacted. IO/useful – Hans/a – c…