陌上人如玉
公子世无双

4.10 React 18 新特性

4.10 React 18 新特性

一句话: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)

十一、面试延伸

  1. 「React 18 最重要的新特性是什么?」
    是「并发渲染(Concurrent Rendering)」——渲染过程可以中断、丢弃、按优先级调度,从而让长渲染不阻塞用户输入。围绕它的一整套 API 包括:createRoot(启用前提)、useTransition / useDeferredValue(标记非紧急更新)、Suspense 的流式 SSR 支持、useSyncExternalStore(避免撕裂)、useId(SSR 一致性)。此外还有自动批处理(所有场景都合并更新)与 StrictMode 更严格的检查。

  2. 「React 18 的自动批处理是什么?」
    指「所有场景下的多次状态更新都会被合并成一次渲染」——包括 setTimeout、Promise.then、原生事件监听里的更新(React 17 及之前这些场景不会批处理)。前提是使用 createRoot。它减少了渲染次数、避免中间态,但也可能改变「依赖渲染次数」的代码行为(升级时需要注意)。被 await 或多个宏任务分隔的更新依然分成多个批次。

  3. 「createRoot 有什么必要?」
    它是启用并发能力的入口。ReactDOM.render 被标记为「legacy 模式」——在 legacy 模式下,React 18 的新特性(自动批处理、并发渲染)都不生效。此外 createRoot 还带来两个变化:① 事件委托目标从 document 改为根容器;② 卸载方式改为 root.unmount()。

  4. 「useTransition 和 useDeferredValue 有什么区别?」
    useTransition 作用于「你主动触发的状态更新」——把 setState 包进 startTransition 回调,并返回 isPending 让你知道「后台更新是否在进行」;useDeferredValue 作用于「传入的值」——返回该值的「滞后版本」,适合「值来自 props/外部、改不了源头」的场景。两者都基于同一套优先级机制,实践中常配合使用。

  5. 「React 18 里 useId 解决什么问题?」
    解决 SSR 下的 ID 一致性问题。自己生成 ID(Math.random()、递增计数器)会导致服务端与客户端渲染出的 HTML 不一致,触发 hydration mismatch。useId 由 React 保证「同一个组件在两端得到相同 ID」,主要用途是「无障碍属性关联」(label 的 htmlFor、aria-describedby)。注意它生成的 ID 含冒号(不能当 CSS 选择器),也不能当 key 或业务 ID。

  6. 「useSyncExternalStore 为什么在 React 18 才出现?」
    因为它要解决的「撕裂(tearing)」问题是并发渲染带来的新问题:在可中断渲染下,外部数据源可能在渲染过程中变化,导致同一次渲染中不同组件读到不同版本。React 17 及之前渲染是同步的,这个问题不突出。React 18 引入 useSyncExternalStore 后,Redux、Zustand、Jotai 等库都改用它来订阅外部 store。

  7. 「React 18 的 SSR 有什么改进?」
    主要两点:① 流式渲染——用 renderToPipeableStream 替代 renderToString,shell(页面骨架)一就绪就开始发送,从而提升 TTFB;② 与 Suspense 配合的渐进式水合——用 <Suspense> 划分边界,慢的部分后到,已就绪的部分可以先显示并交互。这让「首屏可见」与「可交互」都变得更快,也支持「选择性水合(Selective Hydration)」。

  8. 「升级 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 会「挂载 → 卸载 → 再挂载」(暴露清理缺失,要补清理而不是绕过)。

赞(0) 打赏
未经允许不得转载:陌上寒 » 4.10 React 18 新特性

评论 抢沙发

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

微信扫一扫

支付宝扫一扫