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>
}
为什么是标准写法而不是手动管理
以前手动做乐观更新的问题:
- 回滚逻辑容易出错——你得记住“乐观之前的值”是什么
- 并发冲突——两个乐观更新同时进行时,回滚顺序搞不清
- 状态泄漏——更新成功后忘记用真实值替换乐观值
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 |
区分紧急/非紧急 |
你不再需要写 isLoading、error、optimisticValue 这些样板代码。 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])
useTransition 和 useOptimistic 属于事件处理器的领域——它们处理的是“用户做了什么”。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 保持响应、真实、一致。