不要过度使用State 什么是State?一个例子 不同步 无用状态

2025-06-10

不要过度 useState

什么是状态?

一个例子

失去同步

无用状态

useState在 React 提供的所有 Hooks 中,它被认为是最基础的。它也是你最有可能使用的(没有双关语的意思),与 并列useEffect

然而在过去的几个月里,我发现这个钩子被滥用了很多次。这主要与钩子本身无关,而是因为状态管理从来就不是一件容易的事。

这是我称之为useState 陷阱的系列文章的第一部分,在这里我将尝试概述 useState 钩子的常见场景,这些场景可能会以不同的方式得到更好的解决。

什么是状态?

我认为一切都归结于理解状态是什么。或者更准确地说,什么不是状态。为了理解这一点,我们只需查看React 官方文档即可:

针对每条数据提出三个问题:

它是通过 props 从父级传入的吗?如果是,它可能不是状态。
它会随着时间的推移保持不变吗?如果是,它可能不是状态。
你能根据组件中的任何其他状态或 props 来计算它吗?如果是,它可能不是状态。

到目前为止,一切都很简单。将props 赋值给 state(1)是另一个话题,我可能会另写一篇文章专门讨论。如果你完全没有使用 setter(2),那么很明显,我们并没有处理 state。

剩下的就是第三个问题:派生状态。一个可以通过状态值计算出来的值,显然不是它自己的状态。然而,最近我为一位客户回顾了一些代码挑战,发现这正是我经常看到的模式,甚至在资深候选人身上也是如此。

一个例子

练习非常简单,大致如下:从远程端点获取一些数据(带有类别的项目列表)并让用户按类别进行过滤。

大多数时候,国家管理的方式是这样的:

import { fetchData } from './api'
import { computeCategories } from './utils'

const App = () => {
    const [data, setData] = React.useState(null)
    const [categories, setCategories] = React.useState([])

    React.useEffect(() => {
        async function fetch() {
            const response = await fetchData()
            setData(response.data)
        }

        fetch()
    }, [])

    React.useEffect(() => {
        if (data) {
            setCategories(computeCategories(data))
        }
    }, [data])

    return <>...</>
}
Enter fullscreen mode Exit fullscreen mode

乍一看,这似乎没什么问题。你可能会想:我们有一个 effect 来获取数据,还有一个 effect 来保持类别与数据同步。这正是 useEffect hook 的作用(保持同步),那么这种方法有什么不好呢?

失去同步

这实际上可以正常工作,而且也不是完全无法阅读或难以理解。问题在于我们有一个“公开”可用的函数setCategories,未来的开发人员可能会用到。

如果我们打算让我们的类别完全依赖于我们的数据(就像我们在 useEffect 中表达的那样),那么这是个坏消息:

import { fetchData } from './api'
import { computeCategories, getMoreCategories } from './utils'

const App = () => {
    const [data, setData] = React.useState(null)
    const [categories, setCategories] = React.useState([])

    React.useEffect(() => {
        async function fetch() {
            const response = await fetchData()
            setData(response.data)
        }

        fetch()
    }, [])

    React.useEffect(() => {
        if (data) {
            setCategories(computeCategories(data))
        }
    }, [data])

    return (
        <>
            ...
            <Button onClick={() => setCategories(getMoreCategories())}>Get more</Button>
        </>
    )
}
Enter fullscreen mode Exit fullscreen mode

现在怎么办?我们没有可预测的方式来判断什么是“类别”。

  • 页面加载,类别为X
  • 用户点击按钮,类别为Y
  • 如果数据提取重新执行,比如,因为我们正在使用react-query,它具有当你聚焦你的标签或当你重新连接到网络时自动重新提取的功能(这很棒,你应该尝试一下),类别将再次X。

我们现在无意中引入了一个难以追踪的错误,这种错误只会偶尔发生。

无用状态

或许这与其说是 useState 的问题,不如说是对 useEffect 的一个误解:它应该用来同步React 之外的状态。用 useEffect 来同步两个 React 状态很少是正确的。

因此我想做出以下假设:

每当状态设置函数仅在效果中同步使用时,就摆脱该状态!

— TkDodo

这大致基于@sophiebits 最近在推特上发布的内容:

这是非常实用的建议,我甚至想进一步建议,除非我们能证明计算成本高昂,否则我根本不会去记录它。不要过早优化,务必先测量。我们希望在采取行动之前,先证明某个操作确实很慢。想了解更多关于这个主题的信息,我强烈推荐@ryanflorence的这篇文章

在我的世界里,这个例子看起来就像这样:

import { fetchData } from './api'
import { computeCategories } from './utils'

const App = () => {
    const [data, setData] = React.useState(null)
-   const [categories, setCategories] = React.useState([])
+   const categories = data ? computeCategories(data) : []

    React.useEffect(() => {
        async function fetch() {
            const response = await fetchData()
            setData(response.data)
        }

        fetch()
    }, [])
-
-   React.useEffect(() => {
-       if (data) {
-           setCategories(computeCategories(data))
-       }
-   }, [data])

    return <>...</>
}
Enter fullscreen mode Exit fullscreen mode

我们通过将效应量减半来降低复杂性,现在可以清楚地看到类别是如何从数据中得出的。如果其他人想以不同的方式计算类别,则必须在函数内部进行computeCategories。这样,我们就能始终清楚地了解类别是什么以及它们来自哪里。

单一事实来源。

鏂囩珷鏉ユ簮锛�https://dev.to/tkdodo/don-t-over-usestate-4k72
PREV
React Query 和 TypeScript 泛型类型缩小使用启用选项的类型安全乐观更新 useInfiniteQuery 输入默认查询函数
NEXT
在 Vercel 上部署 Node API(Express Typescript)