正确使用useEffect()
有时并不那么容易。想象一下,我们有以下使用Counter
组件的简单应用程序:
import { useState, useEffect } from 'react';
const Counter = ({ onOdd, onEven }) => {
const [count, setCount] = useState(0);
useEffect(
() => {
console.log('Inside useEffect()');
if (count % 2 === 0) {
onEven(count);
} else {
onOdd(count);
}
},
[count, onOdd, onEven]
);
return (
<button
type="button"
onClick={() => setCount(count => count + 1)}
>
{count}
</button>
);
}
const App = () => {
const [isDarkMode, setIsDarkMode] = useState(false);
return (
<div style={{
backgroundColor: isDarkMode ? 'black' : 'white',
}}>
<Counter
onOdd={count => console.log(`Odd count: ${count}`)}
onEven={count => console.log(`Even count: ${count}`)}
/>
<button
type="button"
onClick={() => setIsDarkMode(isDarkMode => !isDarkMode)}
>
Toggle dark mode
</button>
</div>
);
}
export default App;
该应用程序做两件事:
- 它包括一个
Count
按钮,该按钮可将计数器递增1。该组件允许注入两个函数:onOdd
和onEven
。每当计数器发生变化时,就会调用onOdd
或onEven
,具体取决于计数器。。。奇数或偶数 - 还有一个暗模式切换。我添加它的唯一目的是为了使
Counter
重新呈现,而不是更改count
现在,该应用程序有一个怪癖——每当我们切换暗/亮模式时,就会调用onOdd
或onEven
。这是错误的,但可以理解——我们在每个渲染上创建新的函数,因此调用useEffect()
。
我可以想出4种方法来修复这种行为:
- 从
useEffect()
依赖关系中删除onOdd
和onEven
。它会修复行为,但它被认为是一个问题。linter会抱怨它,因为我们正在失去数据的完整性。理论上,如果我们真的改变了这些回调,它们应该重新运行,对吧?那将是";React方式"> - 将回调函数移到
App
组件之外:
const onOdd = count => console.log(`Odd count: ${count}`);
const onEven = count => console.log(`Even count: ${count}`);
const App = () => {
// ...
return (
// ...
<Counter
onOdd={onOdd}
onEven={onEven}
/>
// ...
);
}
这是一个好的、快速的解决方案,但这是可能的,因为我们在这些回调中不使用钩子或状态。如果我们这样做了呢?
- 在
App
组件中使用useCallback()
:
const App = () => {
// ...
const onOdd = useCallback(
count => console.log(`Odd count: ${count}`),
[]
);
const onEven = useCallback(
count => console.log(`Even count: ${count}`),
[]
);
return (
// ...
<Counter
onOdd={onOdd}
onEven={onEven}
/>
// ...
);
}
- 记忆
Counter
组件中的回调函数。如果我们有数千个组件使用Counter
组件,那么这仍然意味着只有一个地方可以存储这些函数。我不确定这是否有意义
React大师如何处理这个问题?我想让这个例子尽可能简单,所以选项#2会很好地工作,可能会更可取。但是,如果我们需要将这些回调保留在App
组件中呢?
是否总是由父组件负责记忆它传递给子组件的所有回调?如果是,总是用useCallback()
或useMemo()
记忆所有作为道具传递的函数(可能还有任何其他对象),这是一种公认的模式吗?
我不是一个反应大师,但我认为前三种方法都有其最佳点,第四种没有意义。唯一需要小心的是第一个,因为从deps中删除函数可能会导致过时状态问题,所以如果你知道自己在做什么,你可以抑制lint警告(我有时会这样做,也知道很多其他人会这样做),因为这里已经广泛讨论过了https://github.com/facebook/react/issues/14920),否则最好避免这种方法
点数2是首选。每次您有纯函数时,请始终尝试将纯函数放在React组件之外,放在其他文件夹中,如utils、misc等。
根据点数3,这是处理React组件内声明的函数的首选方式,总是用*useCallback*
(或者useMemo
,如果您需要在返回函数之前执行计算)来存储它们,在父组件中这样做没有什么不好的。如果你发现自己有几十个或数百个这样的钩子,并且担心代码污染,请考虑自定义钩子可以让你智能地组织代码,你可以在你的应用程序组件中创建一个自定义钩子,比如useMemoizedHandlers,在那里你可以创建和记忆所有的处理程序,并像一样使用它
const {
handler1,
handler2,
handler3
} = useMemoizedHandlers()
选项2和3都是绝对有效且通用的,可根据函数是否具有渲染周期依赖关系互换使用。选项1是一个很大的禁忌。选项4根本不是真正的记忆化——你可以从作为道具传递的函数中创建稳定的引用,但你不能记忆函数本身,因为它们已经被重新创建了。
是否总是由父组件负责记忆它传递给子组件的所有回调?
在应用程序上下文中,我会说是的,因为这是在消费组件的道具上启用React.memo
的唯一方法。然而,库通常会在子级中将函数转换为稳定的ref
,以防用户忘记记忆自己(或者只是改进了DX)。同样,这与记忆化不同,但它确实意味着你可以避免问题中强调的依赖性问题。
总是用useCallback()或useMemo()来记忆作为道具传递的所有函数(可能还有任何其他对象),这是一种公认的模式吗?
你会在React社区中找到记忆最大值和最小值列表,所以很难说有一个公认的标准。一般来说,你可以不去做,直到你需要它,就像你的例子。然而,纯粹从个人经验来看,一旦你出于必要做了几次,它就会开始成为一种习惯,因为它减少了出现这种错误的可能性。