React hook 参数导致无限重新渲染

时间:2021-01-18 20:50:53

标签: reactjs typescript react-hooks fetch use-effect

我有两个自定义的 React 钩子,一个只是另一个的包装器。

第一个是基本的 fetch hook,它返回状态和结果或错误。

interface State<T> {
  status: 'idle' | 'pending' | 'error' | 'success';
  data?: T;
  error?: string;
}

interface Cache<T> {
  [url: string]: T;
}

type Action<T> =
  | { type: 'request' }
  | { type: 'success'; payload: T }
  | { type: 'failure'; payload: string };

export function useFetch<T = unknown>(
  url: string,
  options?: RequestInit,
  cached = false
): State<T> {
  const cache = useRef<Cache<T>>({});
  const cancelRequest = useRef(false);

  const initialState: State<T> = {
    status: 'idle',
    error: undefined,
    data: undefined,
  };

  const fetchReducer = (state: State<T>, action: Action<T>): State<T> => {
    switch (action.type) {
      case 'request':
        return { ...initialState, status: 'pending' };
      case 'success':
        return { ...initialState, status: 'success', data: action.payload };
      case 'failure':
        return { ...initialState, status: 'error', error: action.payload };
      default:
        return state;
    }
  };

  const [state, dispatch] = useReducer(fetchReducer, initialState);

  useEffect(() => {
    const fetchData = async () => {
      dispatch({ type: 'request' });

      if (cache.current[url] && cached) {
        dispatch({ type: 'success', payload: cache.current[url] });
      } else {
        try {
          const res = await fetch(url, options);
          const json = await res.json();
          cache.current[url] = json;

          if (cancelRequest.current) return;

          if (res.status !== 200) {
            dispatch({ type: 'failure', payload: json.error });
          } else {
            dispatch({ type: 'success', payload: json });
          }
        } catch (error) {
          if (cancelRequest.current) return;

          dispatch({ type: 'failure', payload: error.message });
        }
      }
    };

    fetchData();

    return () => {
      cancelRequest.current = true;
    };
  }, [url, options, cached]);

  return state;
}

export default useFetch;

第二个钩子是一个包装器,它提供了一些获取选项,例如包含 useFetch 钩子的 cookie 的标头。这样我就不必一遍又一遍地编写相同的代码来对我的 API 执行一些请求。我称之为 useAPICall

export const useAPICall = <T>(url: string, body?: any, method = 'GET') => {
  return useFetch<T>(
    apiEndpoint + url,
    {
      method,
      body,
      headers: {
        'Content-Type': 'application/json',
        Accept: 'application/json',
        'x-access-token': getCookie('token'),
      },
    },
    false
  );
};

getCookie 从文档中返回名为 token 的 cookie。

这就是我将如何使用 useAPICall 钩子:

const { status, data, error } = useAPICall<Request>('/example/route');

我的问题是,每当我想使用 useAPICalluseFetch 钩子时,在提供选项对​​象作为参数时,它会导致无限重新渲染和最大更新深度超出错误。

我发现问题出在我的 useEffect 钩子中 useFetch 的选项依赖项中,当我从依赖项数组中删除它时,获取请求运行良好。

唯一的问题是我的 linter 告诉我在依赖项数组中包含选项,因为 useEffect 依赖于它。

我现在想知道如何才能正确地做到这一点。

1 个答案:

答案 0 :(得分:1)

问题是将对象文字作为 options 传递会导致每次钩子重新呈现时 useEffect() 的依赖项都会发生变化,这意味着,正如您所经历的,您将重复获取因为每次上次提取刚刚完成时,效果都会使用新的 options 引用再次运行。

为了打破这个循环,你需要

  • 记住您从 options 传入的 useAPICall()
  • 将您的 options 存储在 useRef() 内的 useFetch()

此外,由于您希望获取对 options 的变化敏感,如果您决定采用第二种方法,则每次 {{1} } 钩子渲染,只有在值改变时才使用新的引用。

以下是在 useFetch() 中实现第一种方法的方法,而无需处理 useAPICall()

useFetch()

下面是您如何实现第二种方法的大部分内容,单独留下 export const useAPICall = <T>(url: string, body?: any, method = 'GET') => { const options = { method, body, headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'x-access-token': getCookie('token'), }, }; const ref = useRef(options); if ( ref.current.method !== options.method || ref.current.body !== options.body || ref.current.headers['x-access-token'] !== options.headers['x-access-token'] ) { ref.current = options; } return useFetch<T>( apiEndpoint + url, ref.current, false ); }; ,尽管我已经留下评论,您需要在其中详细说明它的细节:

useAPICall()