【问题标题】:React hook arguments cause infinite rerendering反应钩子参数导致无限重新渲染
【发布时间】:2021-04-23 04:26:36
【问题描述】:

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

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

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 钩子时,同时提供选项对​​象作为参数,它会导致无限重新渲染和超出最大更新深度错误。

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

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

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

【问题讨论】:

  • 是否支持更改选项触发新请求?
  • @PatrickRoberts 是的,因为我想使用 useAPICall 挂钩在标头中提供令牌,而其他一些路由需要动态正文数据和发布请求。
  • 这就是“你想支持为请求提供选项吗?”的答案。问题是您是否关心 useFetch() 钩子的特定用法是否对在调用它的组件的生命周期内更改的选项敏感。
  • @PatrickRoberts 哦,很抱歉我没有正确阅读,我想可能存在一些用例,我更改组件的状态以触发具有不同主体的新获取请求。
  • 好吧,支持这个是可能的,只是不违反钩子规则就更难做对了。我很快就会得到答复。

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


【解决方案1】:

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

为了打破循环,你需要要么

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

此外,由于您希望获取对 options 更改敏感,如果您决定采用第二种方法,则需要在每次 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() 单独留下,尽管我留下了一条评论,您需要在其中充实它的细节:

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

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

function fetchReducer<T>(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;
  }
};

// if (prev == next) { return prev; } else { return next; }
function headersReducer(prev?: HeadersInit, next?: HeadersInit) {
  if (prev === next) {
    return prev;
  }

  if (!prev || !next) {
    return next;
  }

  if (
    // headers are equal
  ) {
    return prev;
  }

  return next;
}

// if (prev == next) { return prev; } else { return next; }
function optionsReducer(prev?: RequestInit, next?: RequestInit) {
  if (prev === next) {
    return prev;
  }

  if (!prev || !next) {
    return next;
  }

  if (
    prev.body === next.body
    && prev.cache === next.cache
    && prev.credentials === next.credentials
    && prev.headers === headersReducer(prev.headers, next.headers)
    && prev.integrity === next.integrity
    && prev.keepalive === next.keepalive
    && prev.method === next.method
    && prev.mode === next.mode
    && prev.redirect === next.redirect
    && prev.referrer === next.referrer
    && prev.referrerPolicy === next.referrerPolicy
    && prev.signal === next.signal
    && prev.window === next.window
  ) {
    return prev;
  }

  return next;
}

export function useFetch<T = unknown>(
  url: string,
  options?: RequestInit,
  cached = false
): State<T> {
  const cache = useRef<Cache<T>>({});
  const optionsRef = useRef<RequestInit>();
  const [state, dispatch] = useReducer(fetchReducer, initialState);

  optionsRef.current = optionsReducer(optionsRef.current, options);
  const currentOptions = optionsRef.current;

  useEffect(() => {
    let cancelRequest = false;

    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, currentOptions);
          const json = await res.json();
          cache.current[url] = json;

          if (cancelRequest) return;

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

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

    fetchData();

    return () => {
      cancelRequest = true;
    };
  }, [url, currentOptions, cached]);

  return state;
}

export default useFetch;

【讨论】:

  • 非常感谢您的解释和修复,我现在已经实现了第一种方法,并且效果很好!有时间我也会尝试第二种方法。
  • @AlexanderHoerl 如果您使用的是 React 16.13 或更高版本,请查看我的 suspense-service 包,它可能会简化您的数据获取。它依赖于 React 的新 &lt;Suspense /&gt; 组件,这是他们的 experimental concurrent mode API 的一部分。
  • 在第二个实现中,为什么你用一个简单的变量替换了cancelRequest ref?这样更有效率吗?
  • 是的,我在 16.13 以上,我什至不知道新的 Suspense 功能,感谢您指出这一点。去看看吧。
  • @AlexanderHoerl 啊你的cancelRequest ref 不能正常工作。重点是将取消保留在它发生的闭包中,但是 ref 将在闭包之间传播您的取消,有效地取消所有未来在效果依赖项发生变化的重新渲染时的所有请求。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-07-26
  • 1970-01-01
  • 2019-11-02
  • 2022-01-22
  • 2021-04-10
  • 2020-03-25
  • 2022-10-23
相关资源
最近更新 更多