✨ EFLx ☁️

Back

C++ 11 后,移动语义、资源管理、函数式编程等特性彻底改变了 C++ “带类的 C” 的偏见。人们称其为“Modern C++”。 其实,近年来 React 范式也开始走向成熟,我认为 React 18 之后的 React 也可以被称为“Modern React”。 这些年来 React 的更新,并非只是多了几个 Hook,而是范式的继续改革。从命令式地手动管理生命周期和状态同步,转向声明式地定义资源与行为。我们离真正的声明式可以更进一步。

传统 React#

过去,我们习惯用 useEffect 处理所有逻辑。为了获取“peek”状态(读取状态但不希望这是响应式的),我们可能在依赖数组中刻意忽略一些依赖,然而 linter 又会阻止我们这样做。eslint-ignore 之类的东西充斥在代码中。

这无疑会导致代码变得难以维护,同时更隐性的影响是,他让编码者的大脑混乱,或者说对框架的逻辑本身有着混乱的认知,这样的状态下,写出来的代码必然不可能是高质量、具有高可读性、现代化的。

而且,useEffect 已经偏离了其初衷:用于同步内部状态与外部系统。甚至,如果要同步外部的 store,useEffect 也完全称不上是一个好的方案。

为了保护重渲染机制下的性能,useCallbackuseMemo 也被大量使用,这种语法冗余的同时也加剧了依赖数组依赖 linter 的问题。

思维模型转变:以 useEffectEvent 为例#

在 Modern React 中,useEffect 必须是响应式的。如果需要读取某个状态,而该状态变更时又不应该重新触发 useEffect,你应该使用 useEffectEvent。 官方文档的示例如下:

showNotification 是一个行为,而非数据的一部分,因此它不应破坏 Effect 的稳定性。 可以用 useEffectEvent 来调用 showNotification,并在 useEffect 中使用这个 event 而不需要将其加入依赖数组。

不过需要注意,useEffectEvent 返回的 Effect Event 函数只能用在 Effects 中(如 useEffect)。

上述例子反映了 Modern React 最底层的思想:让一切回归本源,依赖数组就应该诚实地声明响应式的依赖,开发者不应该为实现本来常见的目的而使用奇技淫巧;

同样地,useEffect 也只应该被用作同步 React 状态与外部系统,而不应该承担其他的职责,尽管这是 React 自始至终都声明地原则,但只有 Modern React 时代,我们才有足够的便利实现这一原则。让我们继续往下看。

现代化的数据获取与同步#

如果使用 useEffect 来获取数据,我们需要手动设置 data, isLoading, error 等状态,手动处理竞态条件。这样做应该被视为一种反模式。 他造成了许多模板代码,还要求业务开发时也要熟练地掌握一些底层问题,这不仅违背了声明式的范式,在实践中要求每处处理都完美得当也是不可能的。更何况这样做还难以实现缓存、去抖动等现代应用所必须的特性。

依照 React 哲学,数据理应是一种资源,而不是副作用。考虑以下两点:

  • 使用 SWRTanStack Query 等库。在 Safe & Consistent SWR 中,我试图说明使用 useSWR 等 Query 库获取与同步数据的最佳方式,不妨看看。
  • 使用 useSyncExternalStore。这是 React 18 引入的底层原语。对于浏览器 API 或使用全局状态库时,不再需要用 useEffect 去监听和设置状态。
    • 而且,它解决了并发渲染下的问题,确保 UI 在任何时间切片读取到的外部数据是一致的,从而为并发渲染这一重要特性开辟道路。在后面我们将会看到这如何改变了组件渲染的思维方式。

派生状态#

你或者身边的人是否写过这样的代码?

function User({ firstName }) {
    const [fullname, setFullname] = useState(null)
    useEffect(() => {
        setFullname(`Kumo ${firstName}`)
    }, [firstName])

    // ...
}
js

React 教导我们,这里不应该用 useEffect,直接派生状态

function User({ firstName }) {
    const fullname = useMemo(() => `Kumo ${firstName}`, [firstName])
    // ...
}
js

等等……还是不对劲!所有开发者都应该了解 React Compiler,对于仅仅为了保证引用稳定性的场景,不用再浪费时间手动 memo 了。

function User({ firstName }) {
    const fullname = `Kumo ${firstName}`
    // ...
}
js

如果不需要明确的对 memorization 行为的控制,请改用 React Compiler 并直接自然地处理派生状态。

并发渲染#

假设我们有一个耗时操作。设置一个 pending 状态,然后显示一个 spinner! 这的确是很自然的想法,然而滥用会导致界面频繁闪烁,从而要求更高级的处理技巧,这又提高了写出好代码的门槛,而且让开发者的注意力集中在本不应该的事情上。

在并发渲染的模式中,UI 的更新是可以分优先级的,且可以被“挂起”的。

组件可以用 use 解包 Promise,在 Promise resolved 前,渲染会回退到最近的 Suspense 边界。 这意味着我们可以像写同步代码一样处理异步数据,不过,这是从底层层面而言的;对于实际应用,还是建议通过 useSWR 等机制来处理。

什么是 Suspense?我记得 Suspense 以前不是干这个的吧?

你说得没错。Suspense 在 Modern React 中承担起了新的职责,正如上面所述。一旦开始使用 Suspense 而不是在组件内处理 pending 状态,我们就可以回头看看这些场景:

  • 用户在输入框打字,然而 Select 组件的搜索和渲染带有一定延迟
  • 打开 dropdown menu 时需要抓取数据,但显示一个 spinner 显然观感不好;让 router 或 context 传递数据又是反模式

这意味着 UI 渲染是可以延后的。useTransition 展示了如何做到这一点。 例如,在 dropdown menu 的数据获取中,为 useSWR 使用 { suspense: true } 选项,然后用 startTransition 包裹打开 dropdown menu 的逻辑。 你可以用 startTransition 来渲染需要 suspense 的 UI,而这不会真的回到 Suspense 边界;React 会标记这些更新为非紧急,只有在完成后才会显示界面,从而避免了屏幕闪烁的问题。

此外,transitions 可以广泛地应用于需要不阻塞地更新的场景:

function ScrollTracker() {
  const [scrollY, setScrollY] = useState(0)
  useEffect(() => {
    const handler = () => {
      startTransition(() => setScrollY(window.scrollY))
    }
    window.addEventListener('scroll', handler, { passive: true })
    return () => window.removeEventListener('scroll', handler)
  }, [])
}
js

setScrollY 这种操作 UI 状态的行为封装进 startTransition,就能确保更新不会 block UI 了。

Action 同样可用于客户端应用#

很多人说这些 API 是 RSC 带来的,我并不认可。即使在纯客户端应用中,React 19 的 Action API 依然极具革命性。

  1. <form onSubmit={handleSubmit}>
  2. handleSubmitpreventDefault
  3. setPending(true),或许看了前面的内容会考虑 startTransition…?
  4. try/catch POST
  5. setPending(false) / setError(...)
  6. 啊……对!还有乐观更新该怎么办 🤔🤯

相信你已经对这些模板代码感到反感。submit 作为数据的 mutation,不应该这么复杂。

  • useActionState 自动管理异步操作的结果和状态,只需要提供一个处理函数就行!
  • useFormStatus 在 button 等 form 内组件访问 form 状态。
  • useOptimistic 让我们拥抱现代化 UX!立即在 UI 上显示更新;如果失败,自动回滚。

最终的对比#

现代 React 应该能够这样构建应用:

你可能已经忘了在这一切以前,宇宙是一片混沌,造物主因为 messy code 放弃了人类。

总而言之,在传统 React 中,为了实现同样的功能,需要开发者手动编写大量的“胶水代码”来维护状态的一致性。 代码量不仅大,而且每一个手动维护的状态都是潜在的 Bug 来源。

在这个框架之争愈演愈烈的时代,请用更少的代码,更强的鲁棒性,easy 地做正确的事,让我们为 React 正名。

(简体中文) Modern React on Client Side
https://eflx.top/blog/modern-react-client-side/zh-hans
Author EFL
Published at January 23, 2026