I’ve always thought of the useEffect as a lifecycle. Until a while ago, when I used useEffect in a project, there was a problem of repeated requests. After reading relevant articles, I got a new understanding of useEffect.

Request data can be divided into two types: the first type only initializes the request data on the page; The second is to request data based on external changes (such as changes in props or state).

The page initializes request data

The second argument to effets is [], which means that there are no dependencies and effects will only be executed on the first render.

function SearchResults() { const [data, setData] = useState({ hits: [] }); useEffect(() => { const fetchData async () => { const result = await axios( 'https://hn.algolia.com/api/v1/search?query=react', ); setData(result.data); } fetchData(); } []); / /...Copy the code

You might want to move getFetchUrl outside of Effects to reuse logic

function SearchResults() { const [data, setData] = useState({ hits: [] }); funcyion fetchData(){ async () => { const result = await axios( 'https://hn.algolia.com/api/v1/search?query=react' ); setData(result.data); } } useEffect(() => { fetchData(); } []);Copy the code

If the second effect parameter is set to [], the fetchData dependency is omitted. Should the function dependency be ignored in this case? Effects should not lie about its dependence.

So we specify effects dependency

function SearchResults() {
  const [data, setData] = useState({ hits: [] });

    funcyion fetchData(){
      async () => {
        const result = await axios(
        'https://hn.algolia.com/api/v1/search?query=react'
        );
        setData(result.data);
      }
    }

  useEffect(() => {
    fetchData();
  }, [fetchData]); 

Copy the code

At this point, everything looks perfect, but what happens if you add a timer? The following 🌰 is for testing only!!

🌰: Infinite repeat requests have occurred

function SearchResults() { const [data, setData] = useState({ hits: [] }); const [count, setCount] = useState(0); useEffect(() => { const id = setInterval(() => {setCount(count + 1); }, 1000); return () => clearInterval(id); }, [count]); funcyion fetchData(){ async () => { const result = await axios( 'https://hn.algolia.com/api/v1/search?query=react' ); setData(result.data); } } useEffect(() => { fetchData(); }, [fetchData]);Copy the code

The conut changes, causing the component to render, and effect is executed once after each render, then updates the CONut state in effect to render and trigger effect again.

You should ask the request data dependency has never changed to what would cause effect to perform the request data operation

A common misconception is that “functions never change”. This is clearly not true. In fact, the functions defined within the component change with each rendering.

When the props or state changes, the component will be re-rendered. Each rendering has its own properties, including functions, so every time the component renders the function it depends on, it will perform the corresponding operation.

We can wrap the function using useCallback hook

function SearchResults() {
  const fetchData = useCallback(
    async ()=>{
      const result = await axios(
      'https://hn.algolia.com/api/v1/search?query=react'
      );
      setData(result.data);
  },[])

    useEffect(() => {
      fetchData();
    }, [fetchData]);
}
Copy the code

UseCallback essentially adds a layer of dependency checking. It solves the problem in a different way – we make the function itself change only when needed, rather than removing the dependency on the function.

Request data based on external changes

function SearchResults() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('react');

  useEffect(() => {
    const fetchData async () => {
        const result = await axios(
        'https://hn.algolia.com/api/v1/search?query='+query
        );
        setData(result.data);
    }
    fetchData();
  }, [query]);

  // ...
Copy the code

Use useCallback to move getFetchUrl outside of Effects

function SearchResults() { const fetchData = useCallback(async ()=>{ const result = await axios( 'https://hn.algolia.com/api/v1/search?query='+query ); setData(result.data); }, depends on [query]) / / specified useEffect (() = > {fetchData (); }, [fetchData]); }Copy the code

When fetchData uses query, the dependency needs to be defined in useCallback, and the fetchData operation is performed only when the query changes.

A full 🌰

This will be done in terms of manually triggered requests for data retrieval, error handling, load display, and how to implement reusable custom hooks for data retrieval

longhand

import React, { Fragment, useState, useEffect } from 'react'; import axios from 'axios'; function App() { const [data, setData] = useState({ hits: [] }); const [query, setQuery] = useState('redux'); const [url, setUrl] = useState( 'https://hn.algolia.com/api/v1/search?query=redux', ); const [isLoading, setIsLoading] = useState(false); const [isError, setIsError] = useState(false); useEffect(() => { const fetchData = async () => { setIsError(false); setIsLoading(true); Const result = await axios(url); setData(result.data); } catch (error) { setIsError(true); } setIsLoading(false); }; fetchData(); }, [url]); return ( <Fragment> <form onSubmit={() => setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`); event.preventDefault(); } > <input type="text" value={query} onChange={event => setQuery(event.target.value)} /> <button type="submit">Search</button> </form> {isError && <div>Something went wrong ... </div>} {isLoading ? ( <div>Loading ... </div> ) : ( <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> )} </Fragment> ); } export default App;Copy the code

Custom hooks

Put everything that belongs to the data capture (except query status for input fields, but including load indicators and error handling) into custom hooks without the component knowing the relevant data logic.

const useDataApi = (initialUrl, initialData) => { const [data, setData] = useState(initialData); const [url, setUrl] = useState(initialUrl); const [isLoading, setIsLoading] = useState(false); const [isError, setIsError] = useState(false); useEffect(() => { const fetchData = async () => { setIsError(false); setIsLoading(true); try { const result = await axios(url); setData(result.data); } catch (error) { setIsError(true); } setIsLoading(false); }; fetchData(); }, [url]); return [{ data, isLoading, isError }, setUrl]; }; function App() { const [query, setQuery] = useState('redux'); const [{ data, isLoading, isError }, doFetch] = useDataApi( 'https://hn.algolia.com/api/v1/search?query=redux', { hits: []}); return ( <Fragment> <form onSubmit={event => { doFetch( `http://hn.algolia.com/api/v1/search?query=${query}`, ); event.preventDefault(); }} > <input type="text" value={query} onChange={event => setQuery(event.target.value)} /> <button type="submit">Search</button> </form> {isError && <div>Something went wrong ... </div>} {isLoading ? ( <div>Loading ... </div> ) : ( <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> )} </Fragment> ); } export default App;Copy the code

This is demo and this is how you get data using custom hooks. Hooks themselves know nothing about the API. It receives all parameters externally and manages only the necessary states, such as data, load, and error states. It exposes results and actions for the component to use.

However, the useDataApi is not yet decoupled from the state and operation. Next, we use useReducer for further optimization

State and operation are decoupled

The Reducer Hook returns a state object and a function that changes the state object. This function — called a scheduler function — takes an initial value of an action and state object with a type and an optional payload.

import React, { Fragment, useState, useEffect, useReducer, } from 'react'; import axios from 'axios'; Const dataFetchReducer = (state, action) => {switch (action.type) {case 'FETCH_INIT': return {... state, isLoading: true, isError: false }; case 'FETCH_SUCCESS': return { ... state, isLoading: false, isError: false, data: action.payload, }; case 'FETCH_FAILURE': return { ... state, isLoading: false, isError: true, }; default: throw new Error(); }}; const useDataApi = (initialUrl, initialData) => { const [url, setUrl] = useState(initialUrl); const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: false, isError: false, data: initialData, }); useEffect(() => { const fetchData = async () => { dispatch({ type: 'FETCH_INIT' }); try { const result = await axios(url); dispatch({ type: 'FETCH_SUCCESS', payload: result.data }); } catch (error) { dispatch({ type: 'FETCH_FAIL' }); }}; fetchData(); }, [url]); return [state, setUrl]; };Copy the code

All that remains is the URL. Instead of reading all the states directly in effect, it dispatches an action that describes what happened. This decouples our effect from isLoading, isError, and data states. Effect doesn’t care about updating the state anymore, it just tells us what’s going on. All the updated logic is left to the dataFetchReducer: here is the full demo

conclusion

  1. It is recommended to place functions used only by effect inside effect
  2. If the function is dependent on effect, it needs to be used where the function is defineduseCallbackPack a layer and you can use it toouseMemoTo deal with
  3. Don’t blindly ignore dependencies and use them[]You can go throughuseReducer 和 useCallbackTo optimize operations or remove effect dependencies