如何正确的在useEffect里请求数据

5,713 阅读4分钟

一直以来,我将useEffect看作是生命周期.直到前段时间在项目中使用useEffect出现了重复请求的问题,翻阅了相关文章让我对useEffect有了新的认识.

请求数据大概分为两种情况:第一种只在页面初始化请求数据;第二种是根据外部的变化来请求数据(比如props或者state的变化).

页面初始化请求数据

effect的第二个参数为[],表示没有依赖项,effect只会在初次渲染执行.

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();
  }, []);

  // ...

你可能会想将getFetchUrl 移到effect外部,复用逻辑

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();
  }, []); 

此时effect第二个参数设置为[]时会出现提示:遗漏了fetchData依赖。在这种情况下应该忽略对函数的依赖吗?effect不应该对它的依赖撒谎

于是我们指定effect依赖

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]); 

此时,一切看起来很完美,那如果添加一个计时器会发生什么呢? 以下🌰仅仅用来测试!!!

🌰:出现了无限重复请求

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]); 

count发生了变化,引起组件渲染,effect会在每次渲染后执行一次,然后在effect中更新了count状态引起渲染并再次触发effect。

你应该会问请求数据这个依赖一直没变为什么会引起effect执行请求数据的操作

一个常见的误解是,“函数从来不会改变”。这显然不是事实。实际上,在组件内定义的函数每一次渲染都在变。

当props或者state发生变化,组件会重新渲染,每一次渲染都有属于它自己的所有,包括函数,所以每次组件渲染 effect认为它所依赖的函数发现变化就会去执行对应的操作。

我们可以使用 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]);
}

useCallback本质上是添加了一层依赖检查。它以另一种方式解决了问题 - 我们使函数本身只在需要的时候才改变,而不是去掉对函数的依赖。

根据外部的变化来请求数据

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]);

  // ...

使用useCallback 将getFetchUrl 移到effects外部

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

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

fetchData使用到了query就需要在useCallback定义依赖,query变化才执行fetchData操作.

一个完整的🌰

以下将从手动触发请求获取数据、错误处理、加载显示、如何实现可重用的数据获取自定义hooks这几个方面来实现

普通写法

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);
  // 错误处理
      try {
        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;

自定义hooks

将属于数据获取的所有内容(属于输入字段的查询状态除外,但包括加载指示器和错误处理)放入自定义hooks中,组件无需知道相关的数据逻辑。

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;

这是demo这就是使用自定义hooks获取数据的过程。hooks本身对 API 一无所知。它从外部接收所有参数,只管理必要的状态,例如数据、加载和错误状态。它暴露结果和操作给组件使用。

但是useDataApi还未做到状态与操作的解耦,接下来我们使用useReducer来做进一步优化

状态与操作解耦

Reducer Hook 返回一个状态对象和一个改变状态对象的函数。该函数——称为调度函数——接受一个具有类型和可选负载的动作以及状态对象的初始值。

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];
};

现在effect的依赖只剩下url了。相比于直接在effect里面读取所有状态,它dispatch了一个action来描述发生了什么。这使得我们的effect和isLoadingisErrordata状态解耦。effect不再关心怎么更新状态,它只负责告诉我们发生了什么。更新的逻辑全都交由dataFetchReducer去统一处理:这里是完整demo

总结

  1. 建议将仅被effect使用的函数放到effect里面
  2. 如果函数作为effect依赖,那么需要在定义函数的地方用useCallback包一层,你也可以使用useMemo来处理
  3. 不要盲目的忽视依赖而使用[],你可以通过useReducer 和 useCallback来优化操作或者移除某些effect依赖