React 18 有哪些新特性?
一、是什么
React 18 是 React 的一次重大版本更新,于 2022 年 3 月正式发布。它引入了并发渲染(Concurrent Rendering)作为核心底层能力,并在此基础上提供了一系列新 API 和改进,包括自动批处理、新的渲染入口、并发相关 Hooks、Suspense 增强等。
React 18 的设计理念是渐进式升级——大部分应用可以直接升级且无需改动代码,同时可以按需采用新的并发特性。
二、新的渲染入口
2.1 createRoot
React 18 引入了新的客户端渲染 API createRoot,替代旧的 ReactDOM.render:
// React 17(旧 API,React 18 中仍可用但已废弃)
import ReactDOM from 'react-dom';
ReactDOM.render(<App />, document.getElementById('root'));
// React 18(新 API)
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />);
使用 createRoot 的关键意义:
- 启用 React 18 的所有新特性(包括并发渲染)
- 支持
root.unmount() 卸载整个应用
- 移除了 render 的回调参数(可用
useEffect 替代)
2.2 hydrateRoot
用于服务端渲染(SSR)场景的水合操作:
import { hydrateRoot } from 'react-dom/client';
const root = hydrateRoot(
document.getElementById('root'),
<App />
);
与 createRoot 不同,hydrateRoot 在调用时就传入 JSX,因为初始的客户端渲染需要与服务端输出匹配。
三、自动批处理(Automatic Batching)
3.1 React 17 的批处理局限
在 React 17 中,只有 React 事件处理函数内的状态更新会被批处理,setTimeout、Promise、原生事件中的更新不会:
function Counter() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
// React 17:React 事件中,这两次更新会被批处理(只触发一次渲染)
setCount(c => c + 1);
setFlag(f => !f);
}
function handleAsync() {
setTimeout(() => {
// React 17:setTimeout 中,每次 setState 都会触发一次渲染(共两次)
setCount(c => c + 1);
setFlag(f => !f);
}, 0);
}
return <button onClick={handleClick}>{count}</button>;
}
3.2 React 18 的自动批处理
React 18 使用 createRoot 后,所有状态更新都会自动批处理,无论触发位置:
function Counter() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleAsync() {
setTimeout(() => {
// React 18:自动批处理,只触发一次渲染
setCount(c => c + 1);
setFlag(f => !f);
}, 0);
fetch('/api/data').then(() => {
// Promise 回调中同样自动批处理
setCount(c => c + 1);
setFlag(f => !f);
});
}
return <button onClick={handleAsync}>{count}</button>;
}
如果确实需要在某次更新后立即触发重渲染,可以使用 flushSync:
import { flushSync } from 'react-dom';
function handleClick() {
flushSync(() => {
setCount(c => c + 1);
});
// DOM 已更新
flushSync(() => {
setFlag(f => !f);
});
// DOM 再次更新
}
四、新的 Hooks
4.1 useTransition
将状态更新标记为非紧急(transition),使 React 可以中断该更新以优先处理更紧急的操作(如用户输入):
import { useState, useTransition } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
function handleChange(e) {
setQuery(e.target.value);
startTransition(() => {
setResults(filterLargeList(e.target.value));
});
}
return (
<div>
<input value={query} onChange={handleChange} />
{isPending ? <p>加载中...</p> : <ResultList results={results} />}
</div>
);
}
4.2 useDeferredValue
延迟一个值的更新,使其"滞后"于主要的状态变化:
import { useState, useDeferredValue } from 'react';
function SearchResults({ query }) {
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
return (
<div style={{ opacity: isStale ? 0.6 : 1 }}>
<HeavyList filter={deferredQuery} />
</div>
);
}
4.3 useId
生成在服务端和客户端保持一致的唯一 ID,解决 SSR 水合时 ID 不匹配的问题:
import { useId } from 'react';
function FormField({ label }) {
const id = useId();
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} type="text" />
</div>
);
}
4.4 useSyncExternalStore
用于订阅外部数据源,确保在并发渲染下数据的一致性(主要供第三方状态库使用):
import { useSyncExternalStore } from 'react';
function useWindowWidth() {
return useSyncExternalStore(
(callback) => {
window.addEventListener('resize', callback);
return () => window.removeEventListener('resize', callback);
},
() => window.innerWidth,
() => 1024 // SSR 回退值
);
}
function ResponsiveComponent() {
const width = useWindowWidth();
return <div>窗口宽度:{width}px</div>;
}
4.5 useInsertionEffect
在 DOM 变更之后、布局 effect 之前同步执行,专为 CSS-in-JS 库注入样式设计:
import { useInsertionEffect } from 'react';
function useCSS(rule) {
useInsertionEffect(() => {
const style = document.createElement('style');
style.textContent = rule;
document.head.appendChild(style);
return () => style.remove();
}, [rule]);
}
执行顺序:useInsertionEffect → DOM 变更 → useLayoutEffect → 浏览器绘制 → useEffect。
五、Suspense 增强
5.1 SSR 流式渲染
React 18 的 Suspense 支持服务端的流式 HTML 渲染和选择性水合:
// server.js
import { renderToPipeableStream } from 'react-dom/server';
app.get('/', (req, res) => {
const { pipe } = renderToPipeableStream(
<App />,
{
bootstrapScripts: ['/main.js'],
onShellReady() {
res.statusCode = 200;
res.setHeader('Content-type', 'text/html');
pipe(res);
},
}
);
});
function App() {
return (
<Layout>
<NavBar />
<Suspense fallback={<Spinner />}>
<MainContent />
</Suspense>
<Suspense fallback={<SkeletonSidebar />}>
<Sidebar />
</Suspense>
</Layout>
);
}
流式渲染的工作方式:
- 服务端先发送 Shell(NavBar 等已就绪部分)的 HTML
- 当 Suspense 边界内的组件就绪后,以流的方式发送对应 HTML 片段
- 客户端选择性水合——用户交互的区域优先水合
5.2 Suspense 在客户端的改进
React 18 中 Suspense 在客户端的行为也有变化:挂起的组件不再以"可见但隐藏"的方式渲染,而是在 fallback 状态下不挂载子组件,直到数据就绪。
六、Strict Mode 双重调用
React 18 的严格模式在开发环境下,会对组件执行双重挂载(mount → unmount → mount)来帮助发现 effect 清理逻辑中的问题:
// 开发模式下的执行顺序:
// 1. 组件挂载,effect 执行
// 2. 组件卸载,cleanup 执行
// 3. 组件再次挂载,effect 再次执行
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => connection.disconnect(); // 必须正确清理
}, [roomId]);
这一行为旨在提前暴露:
- 忘记编写 cleanup 函数
- cleanup 函数没有正确镜像 setup 逻辑
- 为 React 未来的 Offscreen API(组件保持状态的挂载/卸载)做准备
七、废弃与移除
八、升级策略
// 最小升级步骤:只需修改入口文件
// 1. 安装 React 18
// npm install react@18 react-dom@18
// 2. 修改入口
import { createRoot } from 'react-dom/client';
const container = document.getElementById('root');
const root = createRoot(container);
root.render(
<StrictMode>
<App />
</StrictMode>
);
// 3. 按需逐步采用并发特性(useTransition, useDeferredValue 等)
React 18 的升级是渐进式的:使用 createRoot 后,现有代码继续以同步方式运行,只有在显式使用并发特性(如 startTransition)时才会进入并发渲染模式。
九、总结
React 18 是 React 并发时代的开端,它为后续的 Server Components、React Compiler 等特性奠定了基础。