React 18新特性

React 18是React团队的最新主要版本,引入了并发渲染等重大改进,为构建更流畅的用户体验提供了基础。

版本发布历程

2021年6月

React 18 Alpha版本发布

2021年12月

React 18 Beta版本发布

2022年3月

React 18 RC版本发布

2022年3月29日

React 18正式版发布

核心特性
  • 并发渲染 - 提升应用响应能力
  • 自动批处理 - 优化渲染性能
  • 新的Hooks API - 更多控制能力
  • SSR改进 - 更快的页面加载
  • 严格模式增强 - 更好的开发体验

如何升级到React 18

# 1. 安装React 18和React DOM 18
npm install react@18 react-dom@18

# 2. 更新入口文件
// 从React 17及更早版本
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
  document.getElementById('root')
);

// 升级为React 18
import React from 'react';
import { createRoot } from 'react-dom/client'; // 注意新的导入方式
import App from './App';

// 获取根DOM元素
const container = document.getElementById('root');

// 创建根
const root = createRoot(container);

// 渲染应用
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
升级注意事项:
  • React 18保持向后兼容,大多数应用无需修改代码即可升级
  • 部分弃用API需要更新(如ReactDOM.render
  • 严格模式在开发环境下行为有所变化
  • 建议先升级到React 17,再升级到React 18

核心新特性详解

1. 并发渲染 (Concurrent Rendering) NEW

并发渲染是React 18最重要的特性,它允许React同时准备多个版本的UI。

并发渲染的优势:

  • 可中断渲染:高优先级更新可以中断低优先级渲染
  • 状态复用:React可以在后台准备新的UI而不影响当前界面
  • 更好的响应性:用户交互不会被长任务阻塞
  • 选择性水合:SSR页面可以逐步变得可交互

使用并发特性

import { startTransition } from 'react';

// 将非紧急的更新标记为"transition"
function SearchBox() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  function handleSearch(value) {
    setQuery(value); // 紧急更新:立即执行

    // 非紧急更新:使用startTransition包裹
    startTransition(() => {
      // 获取搜索结果(可能是耗时的操作)
      fetchResults(value).then(data => {
        setResults(data);
      });
    });
  }

  return (
    <div>
      <input
        value={query}
        onChange={(e) => handleSearch(e.target.value)}
      />
      <ResultsList results={results} />
    </div>
  );
}

并发模式 vs 传统模式

React 17及之前
  • 同步渲染
  • 渲染不可中断
  • 长任务会阻塞UI
  • "瀑布式"渲染
React 18
  • 并发渲染
  • 渲染可中断
  • 优先处理用户交互
  • 并行渲染能力

2. 自动批处理 (Automatic Batching) 改进

React 18扩展了批处理的范围,现在包括事件处理器、Promise、setTimeout和原生事件处理程序中的状态更新。

React 17中的批处理

// React 17:只在React事件处理程序中批处理
function handleClick() {
  setCount(c => c + 1); // 重新渲染
  setFlag(f => !f); // 重新渲染
  // 结果:只重新渲染一次(批处理)
}

// React 17:异步代码中不会批处理
setTimeout(() => {
  setCount(c => c + 1); // 重新渲染
  setFlag(f => !f); // 重新渲染
  // 结果:重新渲染两次(无批处理)
}, 1000);

// React 17:Promise中不会批处理
fetch('/api/data').then(() => {
  setCount(c => c + 1); // 重新渲染
  setFlag(f => !f); // 重新渲染
  // 结果:重新渲染两次(无批处理)
});

React 18中的自动批处理

// React 18:所有场景都自动批处理
function handleClick() {
  setCount(c => c + 1);
  setFlag(f => !f);
  // 结果:只重新渲染一次
}

setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // 结果:只重新渲染一次(React 18新行为)
}, 1000);

fetch('/api/data').then(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // 结果:只重新渲染一次(React 18新行为)
});

// 如果需要立即重新渲染(不推荐),可以使用flushSync
import { flushSync } from 'react-dom';

flushSync(() => {
  setCount(c => c + 1); // 立即重新渲染
});
setFlag(f => !f); // 另一次重新渲染
批处理性能提升

3. 新的Hooks API NEW

useTransition

标记非紧急的状态更新,允许用户界面在更新进行时保持响应。

import { useTransition } from 'react';

function App() {
  const [isPending, startTransition] = useTransition();
  const [filter, setFilter] = useState('');
  const [results, setResults] = useState(initialResults);

  function handleFilterChange(value) {
    // 紧急:立即更新输入框
    setFilter(value);

    // 非紧急:搜索结果可以稍后更新
    startTransition(() => {
      setResults(computeExpensiveResults(value));
    });
  }

  return (
    <div>
      <input
        value={filter}
        onChange={(e) => handleFilterChange(e.target.value)}
      />
      {isPending && <Spinner />}
      <ResultsList results={results} />
    </div>
  );
}

useDeferredValue

延迟更新某个值,类似于防抖,但更适合React的渲染模型。

import { useDeferredValue } from 'react';

function SearchResults({ query }) {
  // query是紧急更新,deferredQuery是非紧急更新
  const deferredQuery = useDeferredValue(query);

  // 根据延迟的值进行昂贵的计算
  const suggestions = useMemo(() => {
    return computeSuggestions(deferredQuery);
  }, [deferredQuery]);

  return (
    <>
      <SearchInput query={query} />
      <SuggestionList suggestions={suggestions} />
    <>
  );
}

useId

生成在服务端和客户端之间稳定的唯一ID,用于无障碍属性。

import { useId } from 'react';

function EmailForm() {
  const id = useId();

  return (
    <>
      <label htmlFor={`${id}-email`}>邮箱</label>
      <input id={`${id}-email`} type="email" />

      <label htmlFor={`${id}-name`}>姓名</label>
      <input id={`${id}-name`} type="text" />
    </>
  );
}

// 在SSR和CSR中生成的ID保持一致
// SSR: :r1:-email, :r1:-name
// CSR: :r1:-email, :r1:-name

useSyncExternalStore

用于在并发渲染中安全地从外部数据源读取数据。

import { useSyncExternalStore } from 'react';

function useOnlineStatus() {
  const isOnline = useSyncExternalStore(
    subscribe, // 订阅函数
    getSnapshot, // 获取当前快照
    getServerSnapshot // 服务端快照(SSR时使用)
  );

  return isOnline;
}

// 具体实现
function subscribe(callback) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

function getSnapshot() {
  return navigator.onLine;
}

function getServerSnapshot() {
  return true; // 服务端渲染时的默认值
}

useInsertionEffect

在DOM变更前同步执行,主要用于CSS-in-JS库。

import { useInsertionEffect } from 'react';

// 供CSS-in-JS库使用,普通应用很少需要
function useCSS(rule) {
  useInsertionEffect(() => {
    // 在DOM变更前注入样式
    const style = document.createElement('style');
    style.textContent = rule;
    document.head.appendChild(style);

    return () => {
      document.head.removeChild(style);
    };
  });

  return rule;
}

4. 服务器端渲染改进 NEW

新的流式SSR API

// React 18之前的SSR
import { renderToString } from 'react-dom/server';
import App from './App';

// 阻塞式渲染
const html = renderToString(<App />);
res.send(\`
  <!DOCTYPE html>
  <html>
    <body>
      <div id="root">\${html}</div>
      <script src="/client.js"></script>
    </body>
  </html>
\`);

// React 18的流式SSR
import { renderToPipeableStream } from 'react-dom/server';
import App from './App';

app.get('/', (req, res) => {
  const { pipe } = renderToPipeableStream(<App />, {
    // Bootstrap脚本
    bootstrapScripts: ['/main.js'],
    // 完成回调
    onShellReady() {
      res.setHeader('Content-type', 'text/html');
      res.write(\`
        <!DOCTYPE html>
        <html>
          <body>
            <div id="root">
      \`);
      pipe(res);
      res.write(\`
            </div>
          </body>
        </html>
      \`);
    },
    onShellError(error) {
      // 错误处理
      res.status(500).send('<h1>出错了</h1>');
    }
  });
});

选择性水合 (Selective Hydration)

import { hydrateRoot } from 'react-dom/client';
import App from './App';

// React 18之前:整个应用一次性水合
hydrate(document.getElementById('root'), <App />);

// React 18:支持选择性水合
const root = hydrateRoot(document.getElementById('root'), <App />);

// 使用Suspense实现部分水合
function App() {
  return (
    <html>
      <head>
        <title>我的应用</title>
      </head>
      <body>
        <Header />
        <Sidebar />
        <main>
          <Suspense fallback={<Spinner />}>
            <Content /> {/* 这个组件可以延迟水合 */}
          </Suspense>
        </main>
        <Footer />
      </body>
    </html>
  );
}

服务端 Suspense

// 组件可以在服务端使用Suspense
async function Content() {
  // 模拟数据获取
  const data = await fetchData();
  return <div>{data}</div>;
}

function App() {
  return (
    <div>
      <Header />
      <Suspense fallback={<div>加载中...</div>}>
        <Content />
      </Suspense>
    </div>
  );
}

// 服务端渲染时,Content组件会先渲染fallback
// 当数据准备好后,再发送实际的HTML

5. 严格模式增强 改进

开发环境下的双重调用

// React 18严格模式在开发环境下:
// 1. 组件会挂载两次
// 2. Effect会运行两次
// 目的是帮助发现副作用相关问题

function MyComponent() {
  console.log('组件渲染'); // 开发环境会打印两次

  useEffect(() => {
    console.log('Effect运行'); // 开发环境会运行两次
    return () => {
      console.log('Effect清理'); // 也会运行两次
    };
  }, []);

  return <div>Hello</div>;
}

// 正确应对双重调用的方式:
function MyComponent() {
  const ref = useRef(null);

  useEffect(() => {
    // 使用ref确保只执行一次
    if (ref.current) return;
    ref.current = true;

    // 实际的副作用逻辑
    const subscription = dataSource.subscribe();

    return () => {
      subscription.unsubscribe();
    };
  }, []);

  // 或者使用idempotent操作
  useEffect(() => {
    // 幂等操作:多次执行结果相同
    document.title = '我的页面';
  }, []);
}

严格模式检测的问题类型

  • 不安全的生命周期:componentWillMount, componentWillUpdate, componentWillReceiveProps
  • 遗留的字符串ref:使用ref="myRef"
  • 遗留的context API:使用旧的context API
  • 意外的副作用:组件渲染不应该有副作用

新API参考表

API 描述 使用场景 替代方案
createRoot 新的应用根节点创建方式 替换ReactDOM.render ReactDOM.render 弃用
hydrateRoot 新的SSR水合方式 替换ReactDOM.hydrate ReactDOM.hydrate 弃用
startTransition 标记非紧急更新 优化用户交互响应性 无直接替代
useTransition 返回过渡状态和启动函数 显示加载状态 无直接替代
useDeferredValue 延迟一个值的更新 防抖/节流场景 useDebounce/useThrottle
useId 生成唯一ID 无障碍属性、表单标签 手动生成ID
useSyncExternalStore 安全读取外部存储 集成外部状态管理 直接订阅
useInsertionEffect DOM变更前执行 CSS-in-JS库 useLayoutEffect
flushSync 强制同步更新 需要立即DOM更新的场景 无直接替代

性能优化最佳实践

1. 合理使用startTransition

// 好的实践:区分紧急和非紧急更新
function SearchPage() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  function handleSearch(newQuery) {
    // 紧急:立即更新输入框
    setQuery(newQuery);

    // 非紧急:搜索结果可以稍后更新
    startTransition(() => {
      searchAPI(newQuery).then(data => {
        setResults(data);
      });
    });
  }

  return (
    <div>
      <input value={query} onChange={e => handleSearch(e.target.value)} />
      {isPending && <Spinner />}
      <ResultsList results={results} />
    </div>
  );
}

2. 使用useDeferredValue优化渲染

// 优化大型列表渲染
function ProductList({ products, searchTerm }) {
  const deferredSearchTerm = useDeferredValue(searchTerm);

  const filteredProducts = useMemo(() => {
    // 根据延迟的搜索词过滤产品
    return products.filter(product =>
      product.name.toLowerCase().includes(deferredSearchTerm.toLowerCase())
    );
  }, [products, deferredSearchTerm]);

  return (
    <ul>
      {filteredProducts.map(product => (
        <ProductItem key={product.id} product={product} />
      ))}
    </ul>
  );
}

3. 并发模式下的Suspense使用

// 使用Suspense实现渐进式加载
function App() {
  return (
    <Suspense fallback={<FullPageSpinner />}>
      <Layout>
        <Navbar />
        <Suspense fallback={<SidebarSkeleton />}>
          <Sidebar />
        </Suspense>
        <Suspense fallback={<ContentSkeleton />}>
          <Content />
        </Suspense>
      </Layout>
    </Suspense>
  );
}

// 配合数据获取
function ProductDetails({ id }) {
  const product = use(fetchProduct(id)); // 假设use是实验性API
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </div>
  );
}

迁移指南

从React 17迁移到React 18

// 步骤1:更新依赖
// package.json
{
  "dependencies": {
    "react": "^18.0.0",
    "react-dom": "^18.0.0"
  }
}

// 步骤2:更新入口文件
// index.js (React 17)
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
  document.getElementById('root')
);

// index.js (React 18)
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';

const container = document.getElementById('root');
const root = createRoot(container);

root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

// 步骤3:处理类型检查
// 如果使用TypeScript,更新类型定义
npm install --save-dev @types/react @types/react-dom

// 步骤4:测试并发特性
// 逐步添加startTransition, useDeferredValue等新特性

常见问题解决

问题 原因 解决方案
组件渲染两次 React 18严格模式 确保副作用是幂等的,或暂时禁用严格模式
控制台警告 使用了弃用的API 更新为新的API(如createRoot
测试失败 测试库不兼容React 18 更新测试库版本,如react-testing-library
TypeScript错误 类型定义不匹配 更新@types/react和@types/react-dom
第三方库兼容性问题 库未适配React 18 等待库更新或使用兼容层

浏览器兼容性

浏览器 支持情况 最低版本
Chrome 完全支持 90+
Firefox 完全支持 89+
Safari 完全支持 14.1+
Edge 完全支持 90+
Internet Explorer 不支持 不支持
升级建议:
  • 新项目:直接使用React 18
  • 已有项目:先升级到React 17,测试通过后再升级到React 18
  • 大型应用:逐步迁移,可以先更新入口文件,再逐步使用新特性
  • 测试覆盖:确保有良好的测试覆盖,避免升级引入问题

React未来展望

React 18为未来的功能奠定了基础:

  • 服务器组件:在服务端运行的React组件
  • 资源加载优化:更智能的资源预加载
  • 更好的开发者工具:并发渲染的可视化调试
  • 更多并发特性:更细粒度的渲染控制