React 19 并发全景:从用户点击到 UI 更新的完整链路

Transition、Suspense、useOptimistic、useFormStatus、useActionState——不是五个独立 API,而是一条完整链路的五个阶段

React 19 的新 API 很多,但它们不是各自独立的功能。它们解决的是同一个问题的不同阶段:用户操作发生后,如何让 UI 在异步世界里保持响应、保持真实、保持一致?

这篇文章不是逐个介绍 API。它是一条完整的链路——从用户点击按钮开始,到最终 UI 更新完成,中间经历的每一个阶段,React 19 都给出了标准化的解决方案。


一、问题:异步操作让 UI 失去控制

用户点了“购买”按钮 → 发请求 → 等响应 → 更新 UI。

这段等待的时间里,UI 应该显示什么?按钮要不要禁用?列表要不要先显示乐观结果?如果失败了怎么回滚?

以前的做法:

function BuyButton() {
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState(null)
  const [optimisticItems, setOptimisticItems] = useState(items)

  async function handleBuy() {
    setIsLoading(true)        // 状态 1:loading
    setError(null)
    setOptimisticItems([...items, newItem])  // 状态 2:乐观更新
    try {
      await buyItem()
      await refreshItems()   // 状态 3:真实数据
    } catch (e) {
      setOptisticItems(items)  // 状态 4:回滚
      setError(e)
    } finally {
      setIsLoading(false)    // 状态 5:结束
    }
  }
  // ...
}

五个状态,散落在一个函数里,每个异步操作都要手写一遍。React 19 要解决的就是:把这些状态管理标准化,从组件里剥离出去。


二、阶段一:区分紧急与非紧急(useTransition)

用户在搜索框里输入 → 触发搜索 → 结果列表更新。

如果结果计算很慢,输入框也会卡住——因为 React 默认把所有 setState 视为同等优先级。用户每敲一个字母,整个 UI 都要等搜索结果算完才能响应。

useTransition 把更新分成两类:

function SearchBox() {
  const [isPending, startTransition] = useTransition()
  const [query, setQuery] = useState('')
  const [results, setResults] = useState([])

  function handleChange(e) {
    setQuery(e.target.value)           // 紧急:输入框立刻响应

    startTransition(() => {
      setResults(search(e.target.value))  // 非紧急:可以被打断
    })
  }

  return (
    <>
      <input value={query} onChange={handleChange} />
      {isPending ? <Spinner /> : <ResultList results={results} />}
    </>
  )
}

内部机制:Lanes 决定谁先执行

React 18 引入的 Lane 模型是这一切的底层基础。每个更新被分配到不同的 Lane,本质上是二进制位掩码:

SyncLane            = 0b0000000000000000000000000000001  // 同步,最高优先级
InputContinuousLane = 0b0000000000000000000000000000100  // 连续输入(拖拽)
DefaultLane         = 0b0000000000000000000000000010000  // 普通更新
TransitionLane      = 0b0000000000000000000001000000000  // startTransition
IdleLane            = 0b0100000000000000000000000000000  // 空闲

setQuery → SyncLane → 立即执行,不可中断 startTransition(() => setResults(...)) → TransitionLane → 低优先级,可以被打断

用户继续输入时,新的 SyncLane 更新可以中断正在执行的 TransitionLane 更新。这就是为什么输入框不会卡住——高优先级任务永远能抢占低优先级任务。

useTransition vs useDeferredValue

两者解决同一个问题(降低优先级),但切入点不同:

// useTransition:控制 setState 的优先级
// "我知道要更新什么状态"
startTransition(() => setResults(search(query)))

// useDeferredValue:创建一个"延迟"版本的值
// "我只控制渲染结果的优先级"
const deferredQuery = useDeferredValue(query)

useDeferredValue 适合你控制不了触发更新的场景(比如 props 传进来的值)。它创建一个“延迟版本”的值,React 在空闲时用这个值渲染,不阻塞紧急更新。

React 19 的关键变化:async startTransition

React 18 的 startTransition 只支持同步函数。React 19 扩展了它:

startTransition(async () => {
  const data = await fetchResults(query)
  setResults(data)
})

Transition 从“优先级调度工具”扩展成了“异步操作管理工具”。这意味着 isPending 可以覆盖整个异步操作的生命周期——从发起请求到数据返回,这段时间里 isPending 都是 true

这个变化意义重大:它让 Transition 成了 React 19 并发模型的核心入口,而不只是一个性能优化 API。


三、阶段二:乐观更新(useOptimistic)

等服务端响应再更新 UI 太慢了。更好的做法:先假设成功,立刻更新 UI,失败了再回滚。

function LikeButton({ initialLikeCount }) {
  const [likeCount, setLikeCount] = useState(initialLikeCount)
  const [optimisticCount, addOptimistic] = useOptimistic(
    likeCount,
    (current, increment) => current + increment
  )

  async function handleLike() {
    addOptimistic(1)          // 立刻 +1,UI 瞬间响应
    try {
      await likePost()        // 发请求
      setLikeCount(c => c + 1)  // 成功 → 更新真实值
    } catch {
      // 失败 → 自动回滚到 likeCount(乐观值消失)
    }
  }

  return <button onClick={handleLike}>♥ {optimisticCount}</button>
}

为什么是标准写法而不是手动管理

以前手动做乐观更新的问题:

  1. 回滚逻辑容易出错——你得记住“乐观之前的值”是什么
  2. 并发冲突——两个乐观更新同时进行时,回滚顺序搞不清
  3. 状态泄漏——更新成功后忘记用真实值替换乐观值

useOptimistic 把这些全部封装了。你给它“真实值”和“更新函数”,它返回“乐观值”。异步操作完成后,乐观值自动同步回真实值。不需要手动管理任何状态转换。

与 Transition 的协作

startTransition(async () => {
  addOptimistic({ text: formData.get('text'), pending: true })  // 乐观更新
  await formAction(formData)                                      // 异步操作
})

Transition 负责优先级——这个操作可以被更紧急的用户输入打断。useOptimistic 负责 UI 反馈——用户立刻看到结果。两者各司其职。


四、阶段三:表单交互标准化

表单是异步操作最密集的地方。提交、验证、错误处理、loading 状态——几乎每个表单都要手写一遍相同的逻辑。

React 19 把这些标准化成三个 API,分别解决表单生命周期的不同问题。

useActionState:管理 action 的结果

表单提交 → 服务端 action → 返回结果。怎么把结果回传给客户端组件?

async function submitForm(prevState, formData) {
  const email = formData.get('email')
  if (!email.includes('@')) {
    return { error: '邮箱格式不正确' }
  }
  await saveEmail(email)
  return { success: true }
}

function ContactForm() {
  const [state, formAction, isPending] = useActionState(submitForm, null)

  return (
    <form action={formAction}>
      <input name="email" type="email" />
      {state?.error && <p className="error">{state.error}</p>}
      {state?.success && <p>提交成功!</p>}
      <button disabled={isPending}>
        {isPending ? '提交中...' : '提交'}
      </button>
    </form>
  )
}

useActionState 接收 action 函数和初始状态,返回三样东西:

  • state — 最新一次 action 的返回值
  • formAction — 包装后的 action,可以直接传给 <form action={...}>
  • isPending — action 是否正在执行

action 函数接收 (prevState, formData),返回新状态。这个设计让表单验证、错误处理、状态更新全部内聚在一个函数里。

useFormStatus:子组件感知表单状态

Submit 按钮通常是个子组件。它需要知道“表单正在提交中”来 disable 自己,但它拿不到父级表单的提交状态。

以前的做法是 prop drilling 或 context。useFormStatus 把这件事标准化了:

function SubmitButton() {
  const { pending } = useFormStatus()
  return (
    <button disabled={pending}>
      {pending ? '提交中...' : '提交'}
    </button>
  )
}

function ContactForm() {
  return (
    <form action={submitAction}>
      <input name="email" type="email" />
      <SubmitButton />  {/* 自动感知表单状态,无需传 prop */}
    </form>
  )
}

注意:这是 react-dom 的 hook,不是 react 的。它只能在 <form> 的子组件里用,自动订阅最近的父级表单状态。

三个 API 的协作

实际场景里这三个 API 经常一起出现:

function CommentForm({ postId }) {
  const [state, formAction, isPending] = useActionState(submitComment, null)
  const [optimisticComments, addOptimistic] = useOptimistic(
    comments,
    (prev, newComment) => [...prev, newComment]
  )

  return (
    <>
      <CommentList comments={optimisticComments} />
      <form action={async (formData) => {
        addOptimistic({ text: formData.get('text'), pending: true })
        await formAction(formData)
      }}>
        <textarea name="text" />
        <SubmitButton />
        {state?.error && <p>{state.error}</p>}
      </form>
    </>
  )
}
  • useOptimistic → 立刻显示新评论(乐观更新)
  • useActionState → 管理服务端响应(成功/失败)
  • useFormStatus → SubmitButton 自动感知 pending 状态

三个 API 分工明确,互不耦合。


五、阶段四:Suspense 控制揭示时机

前面三个阶段处理的是“用户操作 → 异步请求 → UI 更新”。但还有一个问题:数据什么时候到达,UI 什么时候揭示?

Suspense 在 React 19 里的角色不只是“加载中”的 fallback。它是整个并发模型的“揭示控制器”——决定用户在什么时候看到什么内容。

<Suspense fallback={<Skeleton />}>
  <UserProfile userId={id} />
</Suspense>

UserProfile 的数据还没到达时,显示 <Skeleton />。数据到了,切换到真实内容。

但 Suspense 的真正威力在于嵌套

<Suspense fallback={<PageSkeleton />}>
  <Header />                {/* 立即显示 */}
  <Suspense fallback={<ContentSkeleton />}>
    <MainContent />         {/* 数据到了才显示 */}
  </Suspense>
  <Suspense fallback={<CommentsSkeleton />}>
    <Comments />            {/* 独立加载,不阻塞主内容 */}
  </Suspense>
</Suspense>

Header 立即显示,MainContent 和 Comments 各自独立加载。一个慢不阻塞另一个。这就是“渐进式揭示”——UI 的不同部分在不同时间点出现,用户体验比“全有或全无”好得多。

与 RSC 的协作

在 RSC 场景下,Suspense 和 Progressive JSON 配合工作:

服务端渲染 <App>
  → Header 的 JSX 立即序列化发送
  → MainContent 的数据查询慢 → 发送 Suspense 占位符
  → 数据到了 → Progressive JSON 补发 MainContent 的 JSX
  → 客户端收到 → Suspense 切换到真实内容

数据传输和 UI 揭示是解耦的。数据尽快到达,Suspense 控制用户看到什么。


六、完整链路:从用户点击到 UI 更新

把四个阶段串起来,看一个完整场景:用户在评论区点“发送”。

用户点击"发送"

  ├─ 紧急更新(SyncLane)
  │   └─ 清空输入框 → 输入框立即响应

  ├─ 乐观更新(useOptimistic)
  │   └─ 新评论立刻出现在列表里(带 pending 标记)

  ├─ startTransition(TransitionLane)
  │   ├─ useActionState 包装的 formAction 开始执行
  │   ├─ isPending = true → SubmitButton 禁用
  │   └─ 发送请求到服务端

  ├─ 服务端处理
  │   ├─ 验证 → 成功 → 返回新评论数据
  │   └─ 验证 → 失败 → 返回错误信息

  ├─ 成功路径
  │   ├─ useActionState state → { success: true }
  │   ├─ 乐观值自动替换为真实值
  │   ├─ isPending = false → SubmitButton 恢复
  │   └─ transition 结束

  └─ 失败路径
      ├─ useActionState state → { error: '...' }
      ├─ 乐观值自动回滚(评论从列表消失)
      ├─ isPending = false → SubmitButton 恢复
      └─ 显示错误信息

每个阶段都有对应的 API,每个 API 都有明确的职责。 不需要手动管理任何中间状态。


七、设计原则:把异步状态管理标准化

React 19 这批 API 有一个共同的设计意图:把以前散落在组件里的异步状态管理逻辑,标准化成框架内置的原语。

以前(手动) 现在(React 19) 解决的问题
setIsLoading(true/false) isPending(Transition / useActionState / useFormStatus) loading 状态管理
手动 setOptimistic + 回滚 useOptimistic 乐观更新 + 自动回滚
手动维护 error state useActionState 返回值 表单错误处理
prop drilling pending useFormStatus 子组件感知表单状态
所有更新同等优先级 startTransition 区分紧急/非紧急

你不再需要写 isLoadingerroroptimisticValue 这些样板代码。 React 帮你管理了这些状态的完整生命周期——从发起到进行中到完成(成功或失败)到回滚。

这就是为什么这些 API 不是“锦上添花的便利函数”——它们改变了你写异步交互代码的基本范式。从“手动管理状态机”变成了“声明式描述数据流”。


八、事件处理器 vs Effect:不要用错地方

在 React 19 的并发模型里,一个关键的区分变得更重要了:这段代码是“响应用户操作”还是“保持同步”?

// 事件处理器:用户点击时执行(非响应式)
function handleBuy() {
  startTransition(async () => {
    addOptimistic({ bought: true })
    await buyItem()
  })
}

// Effect:roomId 变化时重新连接(响应式)
useEffect(() => {
  const conn = createConnection(roomId)
  return () => conn.disconnect()
}, [roomId])

useTransitionuseOptimistic 属于事件处理器的领域——它们处理的是“用户做了什么”。useEffect 属于同步的领域——它处理的是“系统需要保持什么状态”。

React 19 引入了 useEffectEvent 来处理两者交叉的场景——Effect 里需要读取最新的 props/state,但不希望它们的变化触发 Effect 重新执行:

const onThemeChange = useEffectEvent((theme) => {
  showNotification('Connected!', theme)
})

useEffect(() => {
  const connection = createConnection(serverUrl, roomId)
  connection.on('connected', () => onThemeChange(theme))
  connection.connect()
  return () => connection.disconnect()
}, [roomId])  // 只依赖 roomId,theme 变了不重新连接

useEffectEvent 把非响应式逻辑提取出来,脱离依赖追踪。Effect 只在 roomId 变化时重新执行,但 onThemeChange 内部读到的 theme 永远是最新的。


核心一句话

React 19 的并发 API 不是五个独立功能,而是一条完整链路的五个阶段:useTransition 区分优先级 → useOptimistic 乐观反馈 → useActionState / useFormStatus 标准化表单交互 → Suspense 控制揭示时机。它们共同解决了同一个问题:在异步世界里让 UI 保持响应、真实、一致。