Notes from the useEffect Complete Guide

This is a personal note, not a tutorial. It is recommended to read the original text. Although it is longer, it is still very advantageous to try it two or three times. If you have problems, you can communicate with each other.

Original address: https://overreacted.io/zh-hans/a-complete-guide-to-useeffect/
Author profile: https://overreacted.io/zh-hans/my-decade-in-review/

The difference between useEffect(fn, []) and componentDidMount

The former captures props and states, so no matter how long you wait in the callback function, the props and states you get are still the initial values (timer example). If you need to get real-time data, you can use ref or other methods mentioned later.

The best place to store functions

  1. Not related to States and props, put outside components
  2. Only about a useEffect, put it in this
  3. effect uses functions other than those listed above, wrapped in useCallback.

Every rendering

  1. state and props are completely independent of each frame and are constants for this rendering, including object types. Of course, we can't abandon setState and modify object properties directly
    Functions also render each frame independently, and the parameter passed in when called is the current frame's value, which does not change with component updates.
  2. Both have their own effects, which are the States and props stored inside Effects at the time of the current rendering, which can be understood as effects being part of the component rendering.

effects runtime

React only runs effects after the browser has drawn it.

Delay

  1. If there is a delay in the effect s, the state and props output at the time of execution are still the values at the time of the original invocation and will not change with the component update (principle is closure); However, it is always updated in the didUpdate of the class component. That is, the following two writings are equivalent
function Example(props) {
 useEffect(() => {
   setTimeout(() => {
     console.log(props.counter);
   }, 1000);
 });
 // ...
}
function Example(props) {
 const counter = props.counter;
   useEffect(() => {
       setTimeout(() => {
         console.log(counter);
       }, 1000);
 });
 // ...
}

useRef uses real-time States and effects in effects

The following implementations emulate the behavior in a class:

function Example() {
 const [count, setCount] = useState(0);
 const latestCount = useRef(count);
 useEffect(() => {
      // Set the mutable latest value
      latestCount.current = count; 
      setTimeout(() => {
      // Read the mutable latest value 
          console.log(`You clicked ${latestCount.current} times`); 
     }, 3000);
 });
// ...

Cleanup in effects

Return of effects: Run effects after each browser rendering, calling the function defined in return to do some cleanup first, while the state or props in the function is the last value, that is, the data saved at the time of definition. Assuming that the id is cleared after each subscription, if the first id is 10 and the second is 20, the order in which react would have been executed is:

  1. Clear effect with id 10
  2. Render UI with id 20
  3. effect running id 20

And the truth is

  1. Render UI with id 20
  2. Clear effect with id 10
  3. Browser drawing. We see the UI of {id: 20} on the screen.
  4. effect running id 20

That is, render the frame before clearing the previous one.

useEffect update condition

Providing useEffect with a dependent array parameter (deps) is equivalent to telling react that only these parameters are used for the effector, and react skips executing this effect when all parameters are unchanged when the component is updated. If there is a parameter change, all parameters will be synchronized this time.
How do I write update conditions?

"Make it a hard rule to honestly tell effect dependencies, and list all of them"

  1. The first strategy is to include values within the components used in all effect s in the dependency.
  2. The second strategy is to modify the code inside the effect to ensure that the values it contains change only when needed.

Functional form of setState

When we want to update the state according to the previous state, we can use the function form of setState

useEffect(() => {
    const id = setInterval(() => {
          setCount(c => c + 1);
    }, 1000);
    return () => clearInterval(id);
}, []);

useReducer

One principle: convey only the smallest amount of information in the effect s, as above. Sometimes the above does not solve the problem completely. For example, in the following example, when the step changes, count updates the step size and the timer is cleared and restarted.

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))} />
    </>
  );
}

If you want to avoid restarting the timer, you need to use useReducer.

When you want to update a state and that state update depends on the values of another state, you may need to replace them with useReducer. When you write code like setSomething, it may be time to consider using reducer. Reducr allows you to separate descriptions of what is happening within a component and how the state responds and updates.

We replace effect's step dependency with a dispatch dependency:

import React, { useReducer, useEffect } from 'react';
const initState = {
    count: 0,
    step: 1
}
const reducer = (state, action) => {
    const { count, step } = state
    if (action.type === 'tick') {
        return { count: count + step, step }
    } else if (action.type === 'step') {
        return { count, step: action.step }
    }
}
const Reducer01 = () => {
    const [state, dispatch] = useReducer(reducer, initState)
    const {count, step} = state

    useEffect(() => {
        const t = setInterval(() => {
            dispatch({ type: 'tick' })
        }, 1000)
        return ()=>[
            clearInterval(t)
        ]
    }, [dispatch]) // In fact, this dependency is not writable because dispatch is constant throughout the lifecycle of a component
    
    return (
        <div>
            <h3>Use reducer</h3>
            <p>{count}</p>
            <input onChange={e=>dispatch({type:'step',step:Number(e.target.value)})} />
        </div>
    );
}

export default Reducer01;

step from props

If the step in the above example is from props, you can write the reducer to the component to get the step. However, this pattern invalidates some optimizations, so you should avoid abuse -- reducer generates a new version each time the component renders. useReducer can be understood as Hooks'cheating model. It can separate update logic from describing what happened. The benefit is that this helps me remove unnecessary dependencies and avoid unnecessary effect ectcalls.

const Reducer02 = ({ step }) => {
    const reducer = (state, action) => {
       const { count } = state
        if (action.type === 'tick') {
            return { count: count + step, step }
        } else if (action.type === 'step') {
            return { count, step: action.step }
        }
    }
    
    const [state, dispatch] = useReducer(reducer, initState)
    const { count } = state
    
    useEffect(() => {
        const t = setInterval(() => {
            dispatch({ type: 'tick' })
        }, 1000)
        return () => [
            clearInterval(t)
        ]
    }, [])
    return (......);
}

get data

Automatically get results based on query parameters

An example of getting data is when the query parameter query changes, and useEffect automatically pulls the data.

function SearchResults() {
  const [query, setQuery] = useState('react');

  useEffect(() => {
    function getFetchUrl() {
      return 'https://hn.algolia.com/api/v1/search?query=' + query;    
    }

    async function fetchData() {
      const result = await axios(getFetchUrl());
      setData(result.data);
    }

    fetchData();
  }, [query]); // ✅ Deps are OK
  // ...
}

Reuse method for getting query address

When the getFetchUrl logic needs to be reused in the above example, it is written in a component outside the useEffect, with the function as a dependency, which causes data to be requested for each refresh.

function SearchResults() {
  // 🔴 Re-triggers all effects on every render  
  function getFetchUrl(query) { 
     return 'https://hn.algolia.com/api/v1/search?query=' + query; 
  }
  useEffect(() => {
    const url = getFetchUrl('react');
    // ... Fetch data and do something ...
  }, [getFetchUrl]); // 🚧 Deps are correct but they change too often

  useEffect(() => {
    const url = getFetchUrl('redux');
    // ... Fetch data and do something ...
  }, [getFetchUrl]); // 🚧 Deps are correct but they change too often

  // ...
}

The solutions are as follows:

  1. Write a function outside a component if it does not use values within the component
function getFetchUrl(query) {
  return 'https://hn.algolia.com/api/v1/search?query=' + query;
}
function SearchResults() {
  useEffect(() => {
    const url = getFetchUrl('react');
    // ... Fetch data and do something ...
  }, []); // ✅ Deps are OK

  useEffect(() => {
    const url = getFetchUrl('redux');
    // ... Fetch data and do something ...
  }, []); // ✅ Deps are OK
  // ...
}
  1. Use useCallback and use functions as dependencies.
function SearchResults() {
  // ✅ Preserves identity when its own deps are the same  
  const getFetchUrl = useCallback((query) => {
      return 'https://hn.algolia.com/api/v1/search?query=' + query;  
  }, []);  // ✅ Callback deps are OK
  useEffect(() => {
    const url = getFetchUrl('react');
    // ... Fetch data and do something ...
  }, [getFetchUrl]); // ✅ Effect deps are OK

  useEffect(() => {
    const url = getFetchUrl('redux');
    // ... Fetch data and do something ...
  }, [getFetchUrl]); // ✅ Effect deps are OK

  // ...
}
  1. If query is an internal state of a component that can be changed elsewhere, it can be set as a dependency
function SearchResults() {
  const [query, setQuery] = useState('react');

  // ✅ Preserves identity until query changes
  const getFetchUrl = useCallback(() => {
     return 'https://hn.algolia.com/api/v1/search?query=' + query;  
  }, [query]);  // ✅ Callback deps are OK
  useEffect(() => {
    const url = getFetchUrl();
    // ... Fetch data and do something ...
  }, [getFetchUrl]); // ✅ Effect deps are OK

  // ...
}
  1. If a function is passed in from a parent component, such as a child component that requests data after discovering a query change in the parent component, this still applies to the above method
function Parent() {
  const [query, setQuery] = useState('react');

  // ✅ Preserves identity until query changes
  const fetchData = useCallback(() => {
      const url = 'https://hn.algolia.com/api/v1/search?query=' + query;    // ... Fetch data and return it ...
  }, [query]);  // ✅ Callback deps are OK
  
  return <Child fetchData={fetchData} />
}

function Child({ fetchData }) {
  let [data, setData] = useState(null);

  useEffect(() => {
    fetchData().then(setData);
  }, [fetchData]); // ✅ Effect deps are OK

  // ...
}

It is important to note that this situation cannot be put into a class component because only one function exists throughout the life cycle, the address never changes, and query cannot be driven to refresh as it changes. The solution is to encapsulate a subcomponent to request data, then pass query to the subcomponent, in which query is judged to decide whether to update. In this case, query is equivalent to just diff using the subcomponents, and in hooks, effect s participate in the data flow to better solve this problem.

  1. Similarly, using useMemo allows you to do more with complex objects. In the following example, useMemo encapsulates the stlye format required by a dom, which follows rendering whenever the color changes.
function ColorPicker() {
  // Doesn't break Child's shallow equality prop check
  // unless the color actually changes.
  const [color, setColor] = useState('pink');
  const style = useMemo(() => ({ color }), [color]);
  return <Child style={style} />;
}
  1. It's important to note that the author says, "It's awkward to use useCallback everywhere. In the example above, I prefer to put fetchData in my effects (which can be pulled out into a custom Hook) or bring it in from the top. I want effects to be simple, but calling callbacks inside can make things complicated."

Competition Question

Definition: "The order in which the results of a request are returned is not guaranteed to be consistent. For example, if I first request the data at {id: 10} and then update it to {id: 20}, but the request at {id: 20} returns earlier. Requests earlier but returns later will incorrectly override the status value." The solution is to use a Boolean value to track the state.

function Article({ id }) {
  const [article, setArticle] = useState(null);

  useEffect(() => {
    let didCancel = false;
    async function fetchData() {
      // When this step waits a long time without returning data, the next request occurs.
      // didCancel is set to true in effect Cleanup.
      // Even if this result is returned, it will be discarded, no longer affecting the current data
      const article = await API.fetchArticle(id);
      if (!didCancel) {
          setArticle(article);
      }
    }

    fetchData();

    return () => {
          didCancel = true;
    };
  }, [id]);

  // ...
}

End

The effects described in this article are basically elementary level of use, and the community will introduce some effect hooks to reduce our frequent manual creation of effects.

Keywords: Front-end React hooks

Added by itshim on Fri, 04 Mar 2022 02:47:33 +0200