反应钩子中的 Object.is 相等性检查导致同一函数的多个版本

dag*_*da1 5 typescript reactjs react-hooks

我不确定停止以下情况的正确解决方法是什么。我创建了这个代码沙盒来突出问题。

我有这个钩子,这是一个缩小版本:

export const useAbortable = <T, R, N>(
  fn: () => Generator<Promise<T>, R, N>,
  options: Partial<UseAbortableOptions<N>> = {}
) => {
  const resolvedOptions = {
    ...DefaultAbortableOptions,
    ...options
  } as UseAbortableOptions<N>;
  const { initialData, onAbort } = resolvedOptions;
  const initialState = initialStateCreator<N>(initialData);
  const abortController = useRef<AbortController>(new AbortController());
  const counter = useRef(0);

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

  const runnable = useMemo(
    () =>
      makeRunnable({
        fn,
        options: { ...resolvedOptions, controller: abortController.current }
      }),
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [counter.current]
  );

  const runner = useCallback(
    (...args: UnknownArgs) => {
      console.log(counter.current);

      dispatch(loading);

      runnable(...args)
        .then(result => {
          dispatch(success<N>(result));
        })
        .finally(() => {
          console.log("heree");
          counter.current++;
        });
    },
    [runnable]
  );

  return runner;
};
Run Code Online (Sandbox Code Playgroud)

钩子接受一个函数和选项对象,当它们在每次渲染时重新创建时,钩子使用Object.is比较,无论我做什么,它都会创建返回函数的新版本。

所以我像这样破解它,使用计数器:

  const runnable = useMemo(
    () =>
      makeRunnable({
        fn,
        options: { ...resolvedOptions, controller: abortController.current }
      }),
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [counter.current]
  );
Run Code Online (Sandbox Code Playgroud)

我不得不让 linter 静音才能做到这一点。

短绒建议是这样的:

  const runnable = useMemo(
    () => makeRunnable({ fn, options: { ...resolvedOptions, controller: abortController.current } }),
    [fn, resolvedOptions],
  );
Run Code Online (Sandbox Code Playgroud)

但是,fn并且resolvedOptions每次都会导致创建一个新的可运行函数。

它是具有包裹在一切真正的痛苦useCallbackuseMemo和朋友。

我看过其他 fetch 库,他们正在做其他类似的事情,比如JSON.stringify依赖数组来解决这个问题。

我喜欢钩子,但Object.is平等检查正在扼杀整个范式。

我正确使用依赖项数组的正确方法是什么,这样我就不会每次都得到一个新函数并且让 linter 满意?这两个要求似乎相互增加了可能性。

Shu*_*tri 4

您必须注意,您不需要提供任何 hack 来解决回调问题

ESLint 关于缺少依赖项的警告是为了帮助用户避免在不知不觉中犯下的错误,而不是强制执行。

现在就你的情况而言

如果您查看您的useAbortable函数,您将传递 agenerator functionan options object,现在它们都会在每次重新渲染时创建。

您可以memoize the options传递function给以useAbortable避免依赖关系问题

  • 如果使用,则只需通过提供对 useCallback 的依赖callback pattern for setMessages即可创建一次onAbort[]

  • 生成器函数取决于状态的时间延迟,因此您可以使用 useCallback 创建它并delay作为依赖项提供

      const onAbort = useCallback(() => {
        setMessages(prevMessage => (["We have aborted", ...prevMessage]));
      }, []); // 


      const generator = useCallback(
        function*() {
          const outsideLoop = yield makeFetchRequest(delay, "outside");

          processResult(outsideLoop);

          try {
            for (const request of requests) {
              const result = yield makeFetchRequest(delay, `${request.toString()}`);

              processResult(result);
            }
          } catch (err) {
            if (err instanceof AbortError) {
              setMessages(["Aborted"]);
              return;
            }
            setMessages(["oh no we received an error", err.message]);
          }
        },
        [delay, processResult]
      );

      const options = useMemo(() => ({ onAbort }), [onAbort]);
      const { run, state, abortController, reset, counter, ...rest } = useAbortable<
        Expected,
        void,
        Expected
      >(generator, options);

Run Code Online (Sandbox Code Playgroud)

现在,在内部useAbortable您无需担心fnoptions更改,因为如果我们像上面那样实现,它们只有在绝对有的情况下才会更改

因此,您可以使用正确的依赖项清楚地创建 useAbortable 中的可运行实例

const resolvedOptions = useMemo(() => ({
    ...DefaultAbortableOptions,
    ...options
  }), [options]);

  const runnable = useMemo(
    () =>
      makeRunnable({
        fn,
        options: { ...resolvedOptions, controller: abortController.current }
      }),
    [fn, resolvedOptions]
  );
Run Code Online (Sandbox Code Playgroud)

工作演示