简化 useEffect
1. 写更少的效果
2. 遵循单一职责原则
3. 编写自定义钩子
4. 给它们起名字
5. 不要对依赖关系撒谎
useEffect。每个人都需要这个钩子,但没人想要。根据 React 官方文档,它是“从 React 纯函数式世界进入命令式世界的逃生舱”。Redux作者兼 React 核心团队成员Dan Abramov撰写的useEffect 完整指南需要 49 分钟才能读完——而真正理解它至少需要两倍的时间。
useEffect 在 ReactJs 中已经非常复杂了,几乎没有哪个应用程序可以脱离它来编写。因此,让我们尝试运用一些好的原则,让 useEffect 的使用更易于管理:
1. 写更少的效果
我已经在useState 陷阱系列中写了几种减少影响的方法:
- 在第 1 部分中,我们已经确定某些效果可以用useMemo甚至只是正常的函数执行来替换。
- 在第 2 部分中,我概述了为什么尝试使用useEffect同步不同的反应状态可能是一种反模式,以及您可以做什么。
数据获取
数据获取是一个非常常见的副作用,通常使用useEffect来管理。毕竟,大多数应用都需要从某个地方获取数据。这种情况非常常见,因此有一些非常好的库,它们不仅可以帮助您使复杂的逻辑更具声明性,还可以提供许多很棒的附加功能。
我当然会推荐我最喜欢的开源库React-Query(我怀疑以后再写一篇文章都要提到它了😅),不过SWR、Apollo和RTK-Query也很棒。重点是:不要重复造轮子。有些问题之前已经解决了,值得抽象出来。自从使用 React-Query 以来,我需要编写的 useEffect 代码量大大减少了。
2. 遵循单一职责原则
一个函数或一个类应该只做一件事。你的processPayment
函数最好只处理支付,而不是额外地将用户重定向到某个地方,因为这不是它的责任。同样的原则也适用于你传递给useEffect 的函数。没有必要把所有东西都塞进一个useEffect中:
React.useEffect(() => {
document.title = 'hello world'
trackPageVisit()
}, [])
这里,我们希望在组件“挂载”时执行一些操作,例如设置文档标题并使用某些分析工具跟踪页面访问。虽然乍一看可能没什么用,但我们在这个效果中做了两件截然不同的事情,可以很容易地拆分成两个效果。随着效果依赖关系随时间变化,这种优势将更加明显。
假设我们现在想要添加一个将某些本地状态与文档标题同步的功能:
const [title, setTitle] = React.useState('hello world')
React.useEffect(() => {
document.title = title
trackPageVisit()
}, [title])
你能发现这个 bug 吗?每次标题改变,我们都会跟踪一次页面访问,这可能不是我们想要的。把它拆分成两个 effect 就能解决这个问题,而且我认为我们一开始就应该这么做:
const [title, setTitle] = React.useState('hello world')
React.useEffect(() => {
document.title = title
}, [title])
React.useEffect(() => {
trackPageVisit()
}, [])
现在代码不仅 bug 更少,推理也更简单。每个效果的大小都减少了一半,因此你可以单独查看每个效果,更好地理解其功能。
3. 编写自定义钩子
我真的不喜欢 50% 代码都是钩子调用的组件。这通常表明我们将逻辑和标记混在一起了。将它们隐藏在自定义钩子中除了显而易见的“可以重用它们”之外,还有许多其他好处:
你可以给它们命名
给变量和函数起一个好名字就像写文档一样,Hooks 也一样。如果你使用 TypeScript,清晰定义的接口也会让你受益匪浅:
const useTitleSync = (title: string) => {
React.useEffect(() => {
document.title = title
}, [title])
}
const useTrackVisit = () => {
React.useEffect(() => {
trackPageVisit()
}, [])
}
现在,所有效果都巧妙地隐藏在具有描述性名称的自定义钩子中。我们的组件将只有两行钩子调用,而不是六行,这意味着它可以更专注于其主要职责:生成标记。
你可以封装逻辑
对我来说,这或许是自定义钩子最大的优势:我们可以把原本应该放在一起的东西绑定在一起,而且不必暴露所有内容。useTitleSync钩子并不理想:它只涵盖了效果,每个组件仍然需要手动管理该标题。所以,为什么不把所有与标题相关的内容都放在自定义钩子中,这样就能封装所有逻辑了呢?
const useTitle = (initialTitle: string) => {
const [title, setTitle] = React.useState(initialTitle)
React.useEffect(() => {
document.title = title
}, [title])
return [title, setTitle] as const
}
我们甚至可以更进一步:如果我们只打算在文档标题中显示标题而不在其他地方显示,我们可以将标题值保留在钩子中并仅公开设置器,从而生成最小界面:
const useTitle = (initialTitle: string) => {
const [title, setTitle] = React.useState(initialTitle)
React.useEffect(() => {
document.title = title
}, [title])
return setTitle
}
您可以单独测试它们
测试useTitle钩子而不测试使用它的组件有一个好处,那就是你不必考虑该组件中正在进行的所有其他操作,例如页面跟踪。测试自定义钩子与测试任何其他实用函数非常相似:
import { act, renderHook } from '@testing-library/react-hooks'
describe('useTitle', () => {
test('sets the document title', () => {
const { result } = renderHook(() => useTitle('hello'))
expect(document.title).toEqual('hello')
act(() => result.current('world'))
expect(document.title).toEqual('world')
})
})
4. 给它们起名字
以上所有原因都让我想要编写自定义 hooks,即使我只使用一次。但是,如果你因为某种原因无法或不想将其提取到自定义 hooks 中,传递给useEffect 的函数仍然可以有一个名称,所以可以考虑为你的效果命名:
const [title, setTitle] = React.useState('hello world')
React.useEffect(function syncTitle() {
document.title = title
}, [title])
5. 不要对依赖关系撒谎
甚至对于函数来说,或者说实际上尤其如此。这里我还是听从Dan的建议吧,因为我无法比他的完整指南更好地描述了。
我认为还有一点值得一提:并非所有 effect 都需要依赖项。我见过一些 effect 有 8 个以上的依赖项,其中一些是没有 memoized 的对象,所以它们每次渲染都会触发 effect。所以何必呢,毕竟useEffect的第二个参数是可选的。如果你的 effect 使用提前返回或有条件地执行副作用,这会很方便:
const useInitializePayload = () => {
const payload = usePayload()
React.useEffect(() => {
if (payload === null) {
performSomeSideEffectThatInitializesPayload(value1, value2, ...valueN)
}
})
}
这个效果的依赖数组可能非常大,或者我们可以尝试使用[payload]
依赖项来作弊。我发现这两种方法都比一直运行效果并在必要时中止要差。
希望这些技巧能降低你使用useEffect时的复杂性。请在下方评论区告诉我你更喜欢如何组织你的 effect ⬇️
文章来源:https://dev.to/tkdodo/simplifying-useeffect-5fim