发布于 2025-12-10 0 阅读
0

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

简化 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