次のシナリオを停止するための正しい修正方法はわかりません。問題を強調するために 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],
);
しかし、fn
とresolvedOptions
により、毎回新しい実行可能関数が作成されます。
すべてをuseCallback
、useMemo
、および友達にラップするのは本当に大変です。
他のフェッチライブラリを確認したところ、JSON.stringify
依存関係配列など、この問題を回避するために、次のような他の処理が行われています。
私はフックが好きですが、Object.is
等価チェックはパラダイム全体を殺しています。
依存関係配列を正しく使用して、毎回新しい関数を取得せず、リンターを幸せに保つための正しい方法は何でしょうか? 2つの要件は相互にオッズを追加するようです。
問題はresolvedOptions
とfn
であり、runnable
ではありません。リンターがrunnable
を示唆して修正するため、resolvedOptions
依存関係も書き直す必要があります。
// I assume that `DefaultAbortableOptions` is defined outside of the `useAbortable` hook
const resolvedOptions = useMemo(() => ({
...DefaultAbortableOptions,
...options
}), [options]};
フックは関数とオプションオブジェクトを受け取り、それらは各レンダーで再作成され、フックはObject.is比較を使用するため、何をしても返される関数の新しいバージョンを作成していました。
フックを使用するときは、不必要なものを再作成しないように注意し、useMemo
とuseCallback
をコンポーネント内で定義されたデータと関数に使用してください。
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
関数は、本当に必要な場合(runnable
とoptions
の動的依存関係が変更された場合)にのみ再作成されます。
フックを使用する場合は、フックでオールインし、実際にすべてをラップして、怪しいバグなしで機能するようにする必要があります。私はこれらの特性のため、個人的には嫌いです。
補足: React docsが指摘 のように、useMemo
は、依存関係が変更された場合にのみ実行することが保証されていないため、任意のレンダリングで実行され、runner
が再作成。 Reactの現在のバージョンでは、これは発生しませんが、将来発生する可能性があります。
実際、useCallback
、useMemo
などですべてをラップする必要はありません。
最適化された子コンポーネントがあり、内部関数をプロップとしてそれに渡す場合に使用する必要がある場合、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つの解決策を提案します:
関数をuseMemo
に直接渡すことと、関数式の定義によって関数を作成することの間に違いはないので、次のようにします。
const runnableFunction = () =>
makeRunnable({
fn,
options: { ...resolvedOptions, controller: abortController.current }
});
const runnable = useMemo(
runnableFunction,
[counter.current]
);
このハックJavaScriptを使用することで、変化を感知せず、リンターも満足します。また、フックの正しい使用法も残ります。
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
に。ちょうどドキュメントガイダンスのように。