简化 useEffect 1. 编写更少的效果 2. 遵循单一责任原则 3. 编写自定义钩子 4. 为它们命名 5. 不要对依赖关系撒谎

2025-06-07

简化 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(我怀疑以后再写一篇文章都要提到它了😅),不过SWRApolloRTK-Query也很棒。重点是:不要重复造轮子。有些问题之前已经解决了,值得抽象出来。自从使用 React-Query 以来,我需要编写的 useEffect 代码量大大减少了。

2. 遵循单一职责原则

一个函数或一个类应该只做一件事。你的processPayment函数最好只处理支付,而不是额外地将用户重定向到某个地方,因为这不是它的责任。同样的原则也适用于你传递给useEffect 的函数。没有必要把所有东西都塞进一个useEffect中:

React.useEffect(() => {
    document.title = 'hello world'
    trackPageVisit()
}, [])
Enter fullscreen mode Exit fullscreen mode

这里,我们希望在组件“挂载”时执行一些操作,例如设置文档标题并使用某些分析工具跟踪页面访问。虽然乍一看可能没什么用,但我们在这个效果中做了两件截然不同的事情,可以很容易地拆分成两个效果。随着效果依赖关系随时间变化,这种优势将更加明显。

假设我们现在想要添加一个将某些本地状态与文档标题同步的功能:

const [title, setTitle] = React.useState('hello world')

React.useEffect(() => {
    document.title = title
    trackPageVisit()
}, [title])
Enter fullscreen mode Exit fullscreen mode

你能发现这个 bug 吗?每次标题改变,我们都会跟踪一次页面访问,这可能不是我们想要的。把它拆分成两个 effect 就能解决这个问题,而且我认为我们一开始就应该这么做:

const [title, setTitle] = React.useState('hello world')

React.useEffect(() => {
    document.title = title
}, [title])

React.useEffect(() => {
    trackPageVisit()
}, [])
Enter fullscreen mode Exit fullscreen mode

现在代码不仅 bug 更少,推理也更简单。每个效果的大小都减少了一半,因此你可以单独查看每个效果,更好地理解其功能。

3. 编写自定义钩子

我真的不喜欢 50% 代码都是钩子调用的组件。这通常表明我们将逻辑和标记混在一起了。将它们隐藏在自定义钩子中除了显而易见的“可以重用它们”之外,还有许多其他好处:

你可以给它们命名

给变量和函数起一个好名字就像写文档一样,Hooks 也一样。如果你使用 TypeScript,清晰定义的接口也会让你受益匪浅:

const useTitleSync = (title: string) => {
    React.useEffect(() => {
        document.title = title
    }, [title])
}

const useTrackVisit = () => {
    React.useEffect(() => {
        trackPageVisit()
    }, [])
}
Enter fullscreen mode Exit fullscreen mode

现在,所有效果都巧妙地隐藏在具有描述性名称的自定义钩子中。我们的组件将只有两行钩子调用,而不是六行,这意味着它可以更专注于其主要职责:生成标记。

你可以封装逻辑

对我来说,这或许是自定义钩子最大的优势:我们可以把原本应该放在一起的东西绑定在一起,而且不必暴露所有内容。useTitleSync钩子并不理想:它只涵盖了效果,每个组件仍然需要手动管理该标题。所以,为什么不把所有与标题相关的内容都放在自定义钩子中,这样就能封装所有逻辑了呢?

const useTitle = (initialTitle: string) => {
    const [title, setTitle] = React.useState(initialTitle)

    React.useEffect(() => {
        document.title = title
    }, [title])

    return [title, setTitle] as const
}
Enter fullscreen mode Exit fullscreen mode

我们甚至可以更进一步:如果我们只打算在文档标题中显示标题而不在其他地方显示,我们可以将标题值保留在钩子中并仅公开设置器,从而生成最小界面:

const useTitle = (initialTitle: string) => {
    const [title, setTitle] = React.useState(initialTitle)

    React.useEffect(() => {
        document.title = title
    }, [title])

    return setTitle
}
Enter fullscreen mode Exit fullscreen mode

您可以单独测试它们

测试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')
    })
})
Enter fullscreen mode Exit fullscreen mode

4. 给它们起名字

以上所有原因都让我想要编写自定义 hooks,即使我只使用一次。但是,如果你因为某种原因无法或不想将其提取到自定义 hooks 中,传递给useEffect 的函数仍然可以有一个名称,所以可以考虑为你的效果命名:

const [title, setTitle] = React.useState('hello world')

React.useEffect(function syncTitle() {
    document.title = title
}, [title])
Enter fullscreen mode Exit fullscreen mode

5. 不要对依赖关系撒谎

甚至对于函数来说,或者说实际上尤其如此。这里我还是听从Dan的建议吧,因为我无法比他的完整指南更好地描述了。

我认为还有一点值得一提:并非所有 effect 都需要依赖项。我见过一些 effect 有 8 个以上的依赖项,其中一些是没有 memoized 的对象,所以它们每次渲染都会触发 effect。所以何必呢,毕竟useEffect的第二个参数是可选的。如果你的 effect 使用提前返回或有条件地执行副作用,这会很方便:

const useInitializePayload = () => {
    const payload = usePayload()
    React.useEffect(() => {
        if (payload === null) {
            performSomeSideEffectThatInitializesPayload(value1, value2, ...valueN)
        }
    })
}
Enter fullscreen mode Exit fullscreen mode

这个效果的依赖数组可能非常大,或者我们可以尝试使用[payload]依赖项来作弊。我发现这两种方法都比一直运行效果并在必要时中止要差。


希望这些技巧能降低你使用useEffect时的复杂性。请在下方评论区告诉我你更喜欢如何组织你的 effect ⬇️

文章来源:https://dev.to/tkdodo/simplifying-useeffect-5fim
PREV
TinyML:使用 MicroPython 在 ESP32 上进行机器学习
NEXT
实用的 React Query 客户端状态与服务器状态 React Query