
一句话:React 18 的主线是「并发渲染」——配套
createRoot、自动批处理、useTransition/useDeferredValue/useId/useSyncExternalStore、Suspense 的 SSR 改进,以及 StrictMode 更严格的行为。
一、总览
| 特性 | 内容 | 影响 |
|---|---|---|
createRoot |
新的渲染入口(替代 ReactDOM.render) |
启用并发能力的前提 |
| 自动批处理 | 所有场景(含 setTimeout/Promise/原生事件)都批处理 |
减少渲染次数,可能暴露「依赖渲染次数」的代码 |
useTransition |
把更新标记为「非紧急」 | 长渲染不阻塞输入 |
useDeferredValue |
返回值的「滞后版本」 | 同上,适合 props 场景 |
useId |
生成 SSR 一致的唯一 ID | 无障碍属性关联 |
useSyncExternalStore |
订阅外部数据源的官方方式 | 解决并发下的「撕裂」 |
useInsertionEffect |
为 CSS-in-JS 库设计 | 库作者使用 |
| Suspense 的 SSR 支持 | renderToPipeableStream + 流式渲染 |
首屏更快、可渐进水合 |
| StrictMode 更严格 | 「挂载 → 卸载 → 再挂载」的 Effect 检查 | 暴露清理缺失 |
startTransition(顶层导出) |
可在组件外调用 | 路由等场景 |
二、createRoot:新的渲染入口
// ❌ React 17 的写法(legacy 模式)
import ReactDOM from 'react-dom';
ReactDOM.render(<App />, document.getElementById('root'));
// ✅ React 18+ 的写法
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root')!);
root.render(
<StrictMode>
<App />
</StrictMode>
);
createRoot 的四个变化:
① 启用并发能力(可中断渲染、优先级调度)
② 事件委托的目标从 document 改为【根容器】
③ 卸载用 root.unmount()(不再是 ReactDOM.unmountComponentAtNode)
④ 没有 createRoot 的旧 render 会被标记为「legacy 模式」→
⚠️ 自动批处理等新特性【不生效】
⚠️ 注意:即使用了 createRoot,默认的 setState 仍是 Default/Sync 优先级——
「并发的收益」需要显式使用 transition / Suspense 才能发挥。
三、自动批处理
// React 17:只有合成事件里才批处理
setTimeout(() => {
setA(1); // 渲染 1
setB(2); // 渲染 2
}, 0);
// React 18:所有场景都批处理(包含 setTimeout / Promise / 原生事件)
setTimeout(() => {
setA(1); // 合并
setB(2); // → 只渲染 1 次
}, 0);
⚠️ 被 await / 多个宏任务分隔的更新依然会分成多个批次:
setA(1);
await delay(); // 边界
setB(2); // 另一个批次
⚠️ 升级风险:
如果代码【依赖了「每次 setState 都会触发一次渲染」】,
升级后行为会变(合并成一次)。典型表现是「动画/日志少了一次」。
通常说明这段代码本身就不该依赖渲染次数。
四、并发特性
// ① useTransition:把「非紧急更新」标记出来
function Search() {
const [keyword, setKeyword] = useState('');
const [results, setResults] = useState<Item[]>([]);
const [isPending, startTransition] = useTransition();
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setKeyword(e.target.value); // 紧急
startTransition(() => {
setResults(filterHugeList(e.target.value)); // 非紧急(可被打断)
});
};
return (
<>
<input value={keyword} onChange={handleChange} />
{isPending && <Spinner />}
<List items={results} />
</>
);
}
// ② useDeferredValue:延迟「使用某个值」
function List({ query }: { query: string }) {
const deferredQuery = useDeferredValue(query); // ⭐ 不需要改更新逻辑
const results = useMemo(() => filter(query === deferredQuery ? query : deferredQuery), [deferredQuery]);
const isStale = query !== deferredQuery;
return <div style={{ opacity: isStale ? 0.6 : 1 }}>{results.length} 条结果</div>;
}
// ③ startTransition(顶层导出,可在组件外调用)
import { startTransition } from 'react';
// 常见用法:路由跳转
function useNavigateTransition() {
const navigate = useNavigate();
return (to: string) => {
startTransition(() => navigate(to)); // 导航本身很快,内容渲染可以慢
};
}
五、useId 与 SSR 一致性
// 生成「服务端与客户端一致」的 ID(用于无障碍属性关联)
function Field({ label }: { label: string }) {
const id = useId();
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} aria-describedby={`${id}-hint`} />
<p id={`${id}-hint`}>请输入 2~20 个字符</p>
</div>
);
}
为什么需要它?
① 自己用 Math.random() / 递增计数器 → 服务端与客户端不一致 → hydration mismatch
② useId 由 React 保证两端一致(形如 ':r0:',含冒号所以不能当 CSS 选择器)
③ 它【不能】当 key、不能当业务 ID(语义只是「渲染期唯一」)
六、useSyncExternalStore 与 useInsertionEffect
// ① useSyncExternalStore:订阅外部数据(避免并发下的撕裂)
function useOnlineStatus() {
return useSyncExternalStore(
(cb) => {
window.addEventListener('online', cb);
window.addEventListener('offline', cb);
return () => {
window.removeEventListener('online', cb);
window.removeEventListener('offline', cb);
};
},
() => navigator.onLine, // 客户端快照
() => true // 服务端快照(SSR 必须提供)
);
}
// Redux / Zustand / Jotai 内部都用它
// ② useInsertionEffect:为「CSS-in-JS 库」设计
// 它在「DOM 变更后、layout effect 之前」同步执行,用于插入 <style> 标签
function useCSS(rule: string) {
useInsertionEffect(() => {
const style = document.createElement('style');
style.textContent = rule;
document.head.appendChild(style);
return () => style.remove();
}, [rule]);
}
// ⚠️ 业务代码几乎不需要用它(它是库作者的工具)
七、Suspense 的 SSR 改进
// React 18 之前的 SSR:renderToString(同步、一次性)
// React 18+:流式渲染(Streaming SSR)
// 服务端(Node)
import { renderToPipeableStream } from 'react-dom/server';
app.get('/', (req, res) => {
const { pipe, abort } = renderToPipeableStream(
<App />,
{
onShellReady() {
res.setHeader('content-type', 'text/html');
pipe(res); // ⭐ 「外壳」就绪就开始发送
},
onShellError(err) { res.statusCode = 500; res.send('出错了'); },
onAllReady() { /* 爬虫/静态生成可以等全部就绪 */ },
onError(err) { console.error(err); },
}
);
});
流式 SSR 的三个收益:
① 更快的「首字节时间(TTFB)」
shell(页面骨架)一就绪就发送,不用等所有数据
② 更快的「可交互时间」
配合 Suspense 边界,先水合已就绪的部分
③ 慢的部分可以后到
<Suspense fallback={<Skeleton />}><SlowComments /></Suspense>
→ 评论还在查数据库,但页面其它部分已经可见可交互
⚠️ 需要配合 <Suspense> 边界划分(边界要放在「独立且较慢」的数据块上)
八、StrictMode 的行为变化
// React 18 的 StrictMode 会「挂载 → 卸载 → 再挂载」地重复执行 Effect
useEffect(() => {
const ws = new WebSocket(url);
ws.onmessage = handle;
// ⚠️ 没写清理 → StrictMode 下会创建两个连接(暴露问题)
return () => ws.close(); // ✅ 写清理就没问题
}, [url]);
StrictMode 在 React 18 做了什么(开发环境):
① 组件函数双调用(暴露「渲染不纯」)
② Effect 执行「挂载 → 卸载 → 再挂载」(暴露「清理缺失」)
③ 检测废弃 API
⚠️ 只在开发环境生效;生产构建不会有这些行为。
⚠️ 正确应对方式是「补齐清理」,而不是用 ref 标志位绕过。
九、升级到 React 18 的注意点
① 必须改入口:ReactDOM.render → createRoot(否则新特性不生效)
② 检查「依赖渲染次数」的代码(自动批处理会改变行为)
③ 检查 Effect 的清理函数(StrictMode 会双执行)
④ 第三方库兼容性(尤其是 CSS-in-JS、状态管理库,可能需要升级版本)
⑤ 移除的 API:ReactDOM.render / hydrate / unmountComponentAtNode(提示警告)
⑥ 测试中可能出现「双调用导致的重复请求」→ 需要用 mock 或处理取消
⑦ 服务端渲染改为 renderToPipeableStream(或先用兼容 API)
⑧ TS 类型升级(@types/react 与 @types/react-dom 的版本要匹配)
十、常见坑
// ① 用了 React 18 但没改 createRoot → 自动批处理不生效
ReactDOM.render(<App />, root); // ⚠️ legacy 模式
// ② 期待「createRoot 后所有更新都并发」
// → 默认仍是 Sync/Default 优先级,需要显式用 transition
// ③ 在 transition 里放「紧急反馈」(输入回显、loading)
startTransition(() => setInputValue(v)); // ❌ 输入会卡
// ④ 依赖「每次 setState 都会有渲染」
// ⑤ StrictMode 下没写清理 → 请求发两次、订阅建两个
// ⑥ 用 useId 当 key 或 CSS 选择器
// ⑦ 用 useSyncExternalStore 但 getSnapshot 返回新对象 → 无限循环
// ⑧ SSR 时忘记 getServerSnapshot
// ⑨ 用 useInsertionEffect 做业务逻辑(它是库工具)
// ⑩ 升级后测试挂掉 → 常是「双调用」或「批处理」导致的
// ⑪ @types/react 版本不匹配导致类型报错
// ⑫ 认为「Suspense 只用于 lazy」(它还用于数据获取与流式 SSR)
十一、面试延伸
- 「React 18 最重要的新特性是什么?」
是「并发渲染(Concurrent Rendering)」——渲染过程可以中断、丢弃、按优先级调度,从而让长渲染不阻塞用户输入。围绕它的一整套 API 包括:createRoot(启用前提)、useTransition/useDeferredValue(标记非紧急更新)、Suspense的流式 SSR 支持、useSyncExternalStore(避免撕裂)、useId(SSR 一致性)。此外还有自动批处理(所有场景都合并更新)与 StrictMode 更严格的检查。 -
「React 18 的自动批处理是什么?」
指「所有场景下的多次状态更新都会被合并成一次渲染」——包括setTimeout、Promise.then、原生事件监听里的更新(React 17 及之前这些场景不会批处理)。前提是使用createRoot。它减少了渲染次数、避免中间态,但也可能改变「依赖渲染次数」的代码行为(升级时需要注意)。被await或多个宏任务分隔的更新依然分成多个批次。 -
「
createRoot有什么必要?」
它是启用并发能力的入口。ReactDOM.render被标记为「legacy 模式」——在 legacy 模式下,React 18 的新特性(自动批处理、并发渲染)都不生效。此外createRoot还带来两个变化:① 事件委托目标从document改为根容器;② 卸载方式改为root.unmount()。 -
「
useTransition和useDeferredValue有什么区别?」
useTransition作用于「你主动触发的状态更新」——把setState包进startTransition回调,并返回isPending让你知道「后台更新是否在进行」;useDeferredValue作用于「传入的值」——返回该值的「滞后版本」,适合「值来自 props/外部、改不了源头」的场景。两者都基于同一套优先级机制,实践中常配合使用。 -
「React 18 里
useId解决什么问题?」
解决 SSR 下的 ID 一致性问题。自己生成 ID(Math.random()、递增计数器)会导致服务端与客户端渲染出的 HTML 不一致,触发 hydration mismatch。useId由 React 保证「同一个组件在两端得到相同 ID」,主要用途是「无障碍属性关联」(label的htmlFor、aria-describedby)。注意它生成的 ID 含冒号(不能当 CSS 选择器),也不能当key或业务 ID。 -
「
useSyncExternalStore为什么在 React 18 才出现?」
因为它要解决的「撕裂(tearing)」问题是并发渲染带来的新问题:在可中断渲染下,外部数据源可能在渲染过程中变化,导致同一次渲染中不同组件读到不同版本。React 17 及之前渲染是同步的,这个问题不突出。React 18 引入useSyncExternalStore后,Redux、Zustand、Jotai等库都改用它来订阅外部 store。 -
「React 18 的 SSR 有什么改进?」
主要两点:① 流式渲染——用renderToPipeableStream替代renderToString,shell(页面骨架)一就绪就开始发送,从而提升 TTFB;② 与 Suspense 配合的渐进式水合——用<Suspense>划分边界,慢的部分后到,已就绪的部分可以先显示并交互。这让「首屏可见」与「可交互」都变得更快,也支持「选择性水合(Selective Hydration)」。 -
「升级 React 18 要注意什么?」
五点:① 必须改入口为createRoot(否则新特性不生效);② 检查「依赖渲染次数」的代码(自动批处理会改变行为);③ 补齐 Effect 的清理函数(StrictMode 会「挂载 → 卸载 → 再挂载」);④ 升级第三方库(CSS-in-JS、状态管理库等在并发下可能需要新版本);⑤ 更新类型包(@types/react/@types/react-dom版本要匹配)。测试里若出现「请求发了两次」,通常是双调用暴露出的「未处理取消」问题。
一句话速记
React 18 的主线是并发渲染(可中断 + 优先级),配套:
createRoot(必须改入口,否则新特性全不生效)、自动批处理(setTimeout/Promise/原生事件也合并,但跨await仍分批)、useTransition/useDeferredValue/startTransition、useId(SSR 一致的 ID,不能当 key/选择器)、useSyncExternalStore(防撕裂)、useInsertionEffect(库工具)、Suspense 流式 SSR(renderToPipeableStream);StrictMode 会「挂载 → 卸载 → 再挂载」(暴露清理缺失,要补清理而不是绕过)。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!