我不确定停止以下场景的正确解决方案是什么。我创建了这个代码沙盒来突出这个问题。
我有这个钩子,这里有一个缩小的版本:
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],
);
但是CCD_ 2和CCD_。
不得不把一切都包裹在useCallback
、useMemo
和朋友身上,这真是一种痛苦。
我已经看过其他的获取库,他们正在做其他类似的事情,比如JSON.stringify
依赖数组来解决这个问题。
我喜欢钩子,但Object.is
相等性检查正在扼杀整个范式。
对我来说,正确使用依赖数组的正确方法是什么?这样我就不会每次都得到一个新函数,也不会让linter满意?这两个要求似乎互相增加了矛盾。
您必须注意,您不需要提供任何破解来解决回调问题
ESLint关于缺少依赖项的警告是为了帮助用户避免在不知不觉中犯下的错误,而不是强制执行。
现在在你的情况下
如果您查看useAbortable
函数,您将传递一个generator function
和an options object
,现在它们都是在每次重新渲染时创建的。
您可以将memoize the options
和function
传递给useAbortable
以避免依赖的问题
-
如果使用
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);
现在在useAbortable
中,您不必担心fn
或fn
0会发生变化,因为如果我们像上面的一样实现它,它们只有在绝对发生变化时才会发生变化
因此,您在useAbortable中的可运行实例可以清楚地使用正确的依赖项创建
const resolvedOptions = useMemo(() => ({
...DefaultAbortableOptions,
...options
}), [options]);
const runnable = useMemo(
() =>
makeRunnable({
fn,
options: { ...resolvedOptions, controller: abortController.current }
}),
[fn, resolvedOptions]
);
工作演示
问题出在resolvedOptions
和fn
上,而不是runnable
上。您应该按照linter的建议重写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
和resolvedOptions
0的动态依赖关系发生变化时)才会重新创建。
如果你使用钩子,你必须全力以赴,真正把所有东西都包在钩子里,这样才能让东西在没有可疑虫子的情况下工作。因为这些特点,我个人不喜欢他们。
额外注意:正如React文档所指出的,useMemo
并不能保证只有在其依赖关系发生变化时才能运行,它可能会在任何渲染上运行,并导致runner
被重新创建。在当前版本的React中,这种情况不会发生,但将来可能会发生。
实际上,不需要在useCallback
、useMemo
等中包装所有内容。
关于useCallback
当需要使用时,如果我们有一个优化的子组件,并且我们将一个内部函数作为道具传递给它,那么在每次重新渲染时,函数组件执行上下文中的所有变量和函数都会被评估和重新创建,所以子组件错误地认为传递的道具已经更改,但它没有任何更改,只是重新创建。因此,我们使用useCallback
来记忆内部函数,并传递一个依赖数组来真正重新创建它,但很明显,在每次重新渲染JavaScript时,都会重新评估函数,这是不可避免的。
关于useMemo
,您可以正确地将其用于您的案例,这个钩子用于在每次重新渲染中存储需要更改的变量,可能这些依赖关系不是linter想要的。你真正利用你的依赖,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>;
}
因此,为了更好地使用和省略依赖关系,并拥有愉快的linter,最好像下面这样编写上面的代码:
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
使用内部选项读取当前状态,并通过传递函数而不是表达式来省略依赖关系。因此,在你的情况下,这可能是鼓舞人心的,我建议两种解决方案:
直接将函数传递给
useMemo
或通过函数表达式定义创建函数没有区别,所以这样做吧:const runnableFunction = () => makeRunnable({ fn, options: { ...resolvedOptions, controller: abortController.current } }); const runnable = useMemo( runnableFunction, [counter.current] );
通过使用这个破解,JavaScript感觉不到变化,linter很高兴,而且,你对钩子的正确使用仍然存在。
fn
来自于自定义钩子参数,options
制作的resolvedOptions
也来自于自定义挂钩参数,它们在未来可能都会发生变化。因此,通过ReactJS文档和Dan Abramov的强调,要使用此规则,请将linter所需的依赖项加上您想要的依赖项一起传递:const runnableFunction = () => makeRunnable({ fn, options: { ...resolvedOptions, controller: abortController.current } }); const runnable = useMemo( runnableFunction, [counter.current, fn, resolvedOptions] );
我想最好先传递您想要的依赖项。
对于您当前的情况,我建议使用第一个解决方案,但如果问题是一般性的,我建议采用第二个解决方案。这是文档推荐的。
注意:如果您使用第二个解决方案,请注意这是一个优化的自定义挂钩函数,因此当您在任何地方使用它时,都应该向它传递一个记忆的fn
和options
。就像文档指南一样。