web-dev-qa-db-ja.com

反応フックのObject.is等価チェックにより、同じ関数の複数のバージョンが発生しています

次のシナリオを停止するための正しい修正方法はわかりません。問題を強調するために this codesandbox を作成しました。

私はこのフックを持っており、ここに縮小バージョンがあります:

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

フックは関数とオプションオブジェクトを受け取り、それらは各レンダリングで再作成され、フックはObject.is比較を使用するため、私が何をしても、返される関数の新しいバージョンを作成していました。

だから私はこのようにハッキングして、カウンターを使いました:

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

私はこれを可能にするためにリンターを沈黙させなければなりませんでした。

リンターが示唆するのはこれです:

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

しかし、fnresolvedOptionsにより、毎回新しい実行可能関数が作成されます。

すべてをuseCallbackuseMemo、および友達にラップするのは本当に大変です。

他のフェッチライブラリを確認したところ、JSON.stringify依存関係配列など、この問題を回避するために、次のような他の処理が行われています。

私はフックが好きですが、Object.is等価チェックはパラダイム全体を殺しています。

依存関係配列を正しく使用して、毎回新しい関数を取得せず、リンターを幸せに保つための正しい方法は何でしょうか? 2つの要件は相互にオッズを追加するようです。

5
dagda1

問題はresolvedOptionsfnであり、runnableではありません。リンターがrunnableを示唆して修正するため、resolvedOptions依存関係も書き直す必要があります。

// I assume that `DefaultAbortableOptions` is defined outside of the `useAbortable` hook
const resolvedOptions = useMemo(() => ({
  ...DefaultAbortableOptions,
  ...options
}), [options]};

フックは関数とオプションオブジェクトを受け取り、それらは各レンダーで再作成され、フックはObject.is比較を使用するため、何をしても返される関数の新しいバージョンを作成していました。

フックを使用するときは、不必要なものを再作成しないように注意し、useMemouseCallbackをコンポーネント内で定義されたデータと関数に使用してください。

function MyComponent () {
  const runnable = useCallback(() => {}, [/*let the linter auto-fill this*/])
  const options = useMemo(() => ({}), [/*let the linter auto-fill this*/])
  const runner = useAbortable(runnable, options)
}

このように、runner関数は、本当に必要な場合(runnableoptionsの動的依存関係が変更された場合)にのみ再作成されます。

フックを使用する場合は、フックでオールインし、実際にすべてをラップして、怪しいバグなしで機能するようにする必要があります。私はこれらの特性のため、個人的には嫌いです。

補足: React docsが指摘 のように、useMemoは、依存関係が変更された場合にのみ実行することが保証されていないため、任意のレンダリングで実行され、runnerが再作成。 Reactの現在のバージョンでは、これは発生しませんが、将来発生する可能性があります。

1
Bertalan Miklos

実際、useCallbackuseMemoなどですべてをラップする必要はありません。

最適化された子コンポーネントがあり、内部関数をプロップとしてそれに渡す場合に使用する必要がある場合、useCallbackについて、各再レンダリングで、すべての変数と関数の実行コンテキスト関数コンポーネントが評価および再作成されるため、子コンポーネントは渡されたプロップが変更されたと誤って判断しますが、再作成されたばかりの変更はありません。したがって、useCallbackを使用して内部関数をメモし、依存関係の配列を渡して実際に再作成しましたが、JavaScriptを再レンダリングするたびに関数が再評価され、それが避けられないことは明らかです。

そしてuseMemoについては、あなたはそれをあなたのケースのために正しく使用し、このフックは、いくつかの依存関係を変更する必要がある各レンダーの変数をメモするためのものです、おそらくこれらの依存関係はリンターのものではありません欲求。あなたは本当にあなたの依存関係、あなたの欲望のためにcounter.currentを使います。

リンタールール、つまり react-hooks/exhaustive-deps は、以下のサンプルに設定されています。

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1); // This effect depends on the `count` state
    }, 1000);
    return () => clearInterval(id);
  }, []); // linter error: Obviously `count` is a dependency to this current useEffect

  return <h1>{count}</h1>;
}

したがって、より適切に使用し、依存関係を省略して幸せなリンターを得るには、上記のコードを次のように記述することをお勧めします。

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + 1); // This line doesn't depend on `count` variable outside
    }, 1000);
    return () => clearInterval(id);
  }, []); // happy linter: because there is no dependency

  return <h1>{count}</h1>;
}

ご覧のとおり、setCountは内部オプションを使用して現在の状態を読み取り、式ではなく関数を渡すことで依存関係を省略しています。それであなたのケースで刺激になるかもしれません、私は2つの解決策を提案します:

  1. 関数をuseMemoに直接渡すことと、関数式の定義によって関数を作成することの間に違いはないので、次のようにします。

    const runnableFunction = () =>
      makeRunnable({
        fn,
        options: { ...resolvedOptions, controller: abortController.current }
      });
    
    const runnable = useMemo(
      runnableFunction,
      [counter.current]
    );
    

    このハックJavaScriptを使用することで、変化を感知せず、リンターも満足します。また、フックの正しい使用法も残ります。

  2. fnはカスタムフック引数からのものであり、resolvedOptionsによって作成されたoptionsはカスタムフック引数からのものであり、どちらも将来的に変更される可能性があります。 ReactJS docs の強調と Dan Abramov の強調により、このルールを使用するには、リンターに必要な依存関係を渡しますplus一緒に希望の依存関係:

    const runnableFunction = () =>
      makeRunnable({
        fn,
        options: { ...resolvedOptions, controller: abortController.current }
      });
    
    const runnable = useMemo(
      runnableFunction,
      [counter.current, fn, resolvedOptions]
    );
    

    最初は、必要な依存関係を渡すことをお勧めします。

現在のケースでは、最初のソリューションを使用することをお勧めしましたが、一般的な質問の場合は、2番目のソリューションをお勧めします。ドキュメントで推奨されています。

:2番目のソリューションを使用する場合、これは最適化されたカスタムフック関数であるため、どこで使用する場合でも、記憶されたfnを渡す必要があります。 optionsに。ちょうどドキュメントガイダンスのように。

0
AmerllicA