React 18正式版发布已经几个月了,很多项目已经升级。

但我发现,大部分人升级React 18,只是把ReactDOM.render改成了createRoot,然后就没有然后了。对React 18的新特性,比如并发渲染、useTransition、useDeferredValue、Suspense等,了解不多,更不用说在项目中使用了。

这其实很可惜。React 18的新特性,能实实在在地提升用户体验。本文分享React 18的进阶技巧,包括并发渲染的原理、各个新API的用法、实际应用场景,以及一些容易踩的坑。帮你充分发挥React 18的能力。

一、React 18的核心变化

先说说React 18的核心变化。

1. 并发渲染(Concurrent Rendering)

React 18最大的变化,是引入了并发渲染。

在React 17及之前,渲染是同步的、不可中断的。一旦开始渲染,就必须渲染完,中间不能停。

在React 18中,渲染是并发的、可中断的。React可以:

  • 中断正在进行的渲染
  • 暂停渲染,去处理更高优先级的更新
  • 恢复之前的渲染
  • 丢弃不需要的渲染结果

这意味着,即使有一个很大的渲染任务,用户的交互也不会被阻塞。界面始终保持响应。

2. 自动批处理(Automatic Batching)

React 18中,所有状态更新都会自动批处理。

在React 17中,只有React事件处理函数中的更新会被批处理。在setTimeout、Promise、原生事件中的更新,不会被批处理,每次更新都会触发一次渲染。

在React 18中,不管在哪里更新状态,都会自动批处理。这减少了不必要的渲染,提升了性能。

3. 新的根API

React 18用createRoot替代了ReactDOM.render:

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

// React 18
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

只有用createRoot,才能启用React 18的并发特性。如果还用ReactDOM.render,React 18会降级到React 17的行为模式。

二、useTransition

useTransition是React 18最重要的新API之一。

1. 什么是useTransition

useTransition用来标记"非紧急更新"。

在React 18中,更新分为两种:

  • 紧急更新(Urgent):用户的直接交互,比如点击、输入、拖拽,需要立即响应
  • 过渡更新(Transition):非紧急的更新,比如搜索结果过滤、列表渲染、页面切换,可以延迟

useTransition就是用来把更新标记为过渡更新的。

2. 基本用法

import { useState, useTransition } from 'react';

function SearchResults() {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();

  const handleChange = (e) => {
    // 紧急更新:立即更新输入框的值
    setQuery(e.target.value);
    
    // 过渡更新:延迟更新搜索结果
    startTransition(() => {
      setSearchResults(filterData(e.target.value));
    });
  };

  return (
    <div>
      <input value={query} onChange={handleChange} />
      {isPending ? <Spinner /> : <Results />}
    </div>
  );
}

isPending表示过渡更新是否正在进行中,可以用来显示加载状态。

3. 实际应用场景

useTransition适合这些场景:

  • 搜索过滤:输入框立即响应,搜索结果延迟更新
  • 标签页切换:切换标签时,旧内容保持,新内容加载后再切换
  • 列表渲染:大量数据的列表,渲染不阻塞交互
  • 路由切换:页面切换时,保持当前页面,新页面准备好后再切换

4. 注意事项

  • startTransition中的更新必须是状态更新,不能有副作用
  • 过渡更新可能被中断,所以不要在startTransition中做依赖执行顺序的操作
  • isPending只是一个提示,不保证一定显示,因为过渡更新可能很快完成

三、useDeferredValue

useDeferredValue是另一个并发相关的API。

1. 什么是useDeferredValue

useDeferredValue用来延迟一个值的更新。

和useTransition类似,但useDeferredValue是针对值的,而不是针对更新的。当你无法控制更新的源头(比如值来自父组件或第三方库),可以用useDeferredValue来延迟它。

2. 基本用法

import { useState, useDeferredValue } from 'react';

function SearchResults({ query }) {
  // 延迟query的更新
  const deferredQuery = useDeferredValue(query);
  
  // 用deferredQuery来过滤数据
  const results = filterData(deferredQuery);
  
  return <Results data={results} />;
}

当query变化时,deferredQuery不会立即更新,而是延迟到紧急更新处理完之后。这样,输入框的响应不会被搜索结果的渲染阻塞。

3. 和useTransition的区别

对比项useTransitionuseDeferredValue
针对的对象更新(函数)
控制方式主动标记更新被动延迟值
适用场景你能控制更新你不能控制更新来源
加载状态有isPending没有(可以用useMemo对比)

简单说:能控制更新就用useTransition,不能控制就用useDeferredValue。

4. 配合React.memo使用

useDeferredValue配合React.memo,可以避免子组件的不必要渲染:

const Results = React.memo(function Results({ query }) {
  const data = filterData(query);
  return <List data={data} />;
});

function Search({ query }) {
  const deferredQuery = useDeferredValue(query);
  return <Results query={deferredQuery} />;
}

当query变化时,deferredQuery还没更新,Results不会重新渲染。等deferredQuery更新后,Results才渲染。这样输入框的响应就不会被阻塞。

四、Suspense的增强

React 18对Suspense做了很大增强。

1. Suspense是什么

Suspense是React的一个组件,用来在组件加载时显示降级UI。

<Suspense fallback={<Spinner />}>
  <SomeComponent />
</Suspense>

当SomeComponent还在加载时,显示Spinner。加载完成后,显示SomeComponent。

在React 17中,Suspense只支持代码分割(React.lazy)。在React 18中,Suspense支持了数据获取,并且和并发渲染深度集成。

2. Suspense和并发渲染

在React 18中,Suspense和并发渲染结合,可以实现:

  • 选择性Hydration:SSR时,优先Hydrate用户交互的部分
  • 流式SSR:服务端可以流式发送HTML,边发边渲染
  • 过渡中的Suspense:切换页面时,保持旧页面,新页面加载完成后再切换

3. Suspense的实际用法

import { Suspense, useState } from 'react';

function App() {
  const [tab, setTab] = useState('home');
  
  return (
    <div>
      <TabBar onChange={setTab} />
      <Suspense fallback={<Spinner />}>
        {tab === 'home' && <Home />}
        {tab === 'profile' && <Profile />}
        {tab === 'settings' && <Settings />}
      </Suspense>
    </div>
  );
}

切换标签时,如果新标签的组件还在加载,会显示Spinner,而不是白屏。

配合useTransition,可以实现更平滑的切换:

const [isPending, startTransition] = useTransition();

const handleTabChange = (tab) => {
  startTransition(() => {
    setTab(tab);
  });
};

这样,切换标签时,旧内容会保持显示,直到新内容加载完成。isPending可以用来显示一个轻微的加载指示器。

五、并发特性的实际应用

说说并发特性在实际项目中的应用。

1. 搜索框优化

这是最常见的应用场景。

function Search() {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();
  
  const handleChange = (e) => {
    const value = e.target.value;
    setQuery(value);
    startTransition(() => {
      setResults(filterLargeData(value));
    });
  };
  
  return (
    <div>
      <input value={query} onChange={handleChange} />
      {isPending && <span>搜索中...</span>}
      <Results data={results} />
    </div>
  );
}

效果:输入框立即响应,搜索结果在后台渲染,不阻塞输入。

2. 大列表优化

渲染大量数据的列表时,用useDeferredValue:

function LargeList({ items }) {
  const deferredItems = useDeferredValue(items);
  return <List items={deferredItems} />;
}

当items变化时,列表的渲染被延迟,不阻塞其他交互。

3. 表单提交优化

表单提交时,用useTransition标记提交状态:

function Form() {
  const [isPending, startTransition] = useTransition();
  const [submitted, setSubmitted] = useState(false);
  
  const handleSubmit = (e) => {
    e.preventDefault();
    startTransition(() => {
      submitForm().then(() => setSubmitted(true));
    });
  };
  
  return (
    <form onSubmit={handleSubmit}>
      {/* 表单字段 */}
      <button disabled={isPending}>
        {isPending ? '提交中...' : '提交'}
      </button>
    </form>
  );
}

4. 路由切换优化

配合React Router,实现平滑的路由切换:

function App() {
  const [isPending, startTransition] = useTransition();
  
  const navigate = useNavigate();
  
  const handleNavigate = (path) => {
    startTransition(() => {
      navigate(path);
    });
  };
  
  return (
    <div>
      <Nav onNavigate={handleNavigate} />
      {isPending && <LoadingBar />}
      <Suspense fallback={<Spinner />}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
        </Routes>
      </Suspense>
    </div>
  );
}

六、性能优化技巧

说说React 18中的性能优化技巧。

1. 合理使用useMemo和useCallback

React 18的并发渲染,让useMemo和useCallback更重要了。

因为渲染可能被中断和恢复,如果每次渲染都创建新的对象和函数,会增加GC压力,也可能导致子组件不必要的渲染。

// 好的做法
const handleClick = useCallback(() => {
  doSomething(id);
}, [id]);

const data = useMemo(() => processData(rawData), [rawData]);

但也不要过度使用useMemo和useCallback。只有在确实需要的时候才用,比如:

  • 传递给React.memo的子组件
  • 作为useEffect的依赖
  • 计算开销很大的值

2. 拆分组件

把大组件拆成小组件,可以减少不必要的渲染。

在React 18中,并发渲染可以让小组件的更新不影响其他部分。合理拆分组件,能更好地利用并发特性。

3. 使用React.memo

对于纯展示组件,用React.memo包裹,可以避免不必要的渲染。

const Item = React.memo(function Item({ item }) {
  return <li>{item.name}</li>;
});

配合useCallback和useMemo,效果更好。

4. 虚拟列表

大量数据的列表,用虚拟列表(react-window、react-virtualized)只渲染可见区域。

这在React 18中依然重要,并发渲染不能替代虚拟列表。

七、容易踩的坑

说说React 18中容易踩的坑。

1. StrictMode双重渲染

React 18的StrictMode会双重渲染组件和双重执行useEffect,用来检测副作用问题。

这不是Bug,是有意为之。如果你的代码在StrictMode下出问题,说明你的副作用有问题,需要修复。

2. useEffect执行多次

在React 18中,useEffect可能执行多次(StrictMode下,或并发渲染中断恢复时)。

所以,useEffect中的副作用必须是幂等的,或者有正确的清理函数。

// 错误:没有清理,可能重复订阅
useEffect(() => {
  subscribe();
}, []);

// 正确:有清理函数
useEffect(() => {
  const subscription = subscribe();
  return () => subscription.unsubscribe();
}, []);

3. 在渲染中做副作用

不要在渲染过程中做副作用(比如直接修改DOM、发请求、修改全局变量)。

在React 18中,渲染可能被中断和重复执行,如果在渲染中做副作用,可能会执行多次。

副作用应该放在useEffect或事件处理函数中。

4. 依赖flushSync

flushSync可以强制同步更新,但它会禁用并发渲染,影响性能。

只有在确实需要同步更新的时候才用(比如测量DOM尺寸后立即更新),不要滥用。

5. 第三方库兼容性

一些老的第三方库可能不兼容React 18的并发渲染。升级前要测试。

如果库有问题,可以:

  • 升级到支持React 18的版本
  • 用flushSync包裹库的更新
  • 暂时关闭并发特性(不推荐)

八、迁移建议

给一些从React 17迁移到React 18的建议。

1. 逐步迁移

不要一次性改所有代码。先升级依赖,用createRoot替代render,确保基本功能正常。然后逐步引入并发特性。

2. 开启StrictMode

开发环境开启StrictMode,它能帮你发现潜在问题。虽然会有一些"双重执行"的现象,但这是在帮你检测代码质量。

3. 修复副作用

检查所有useEffect,确保:

  • 有正确的清理函数
  • 副作用是幂等的
  • 不在渲染中做副作用

4. 引入并发特性

从简单的场景开始,比如搜索框、标签页切换,逐步引入useTransition和useDeferredValue。

5. 测试

充分测试,尤其是:

  • 快速交互(快速输入、快速点击)
  • 大列表渲染
  • 网络延迟场景
  • 错误边界

九、写在最后

React 18的并发渲染,是React自Hooks以来最大的变化。

它不是一个"用了就立刻变快"的魔法,而是一套新的编程模型。理解了并发渲染的原理,学会了useTransition、useDeferredValue、Suspense等API,才能真正发挥React 18的能力。

2022年了,React 18已经正式发布,越来越多的项目会升级。希望这篇文章,能帮你更好地理解和使用React 18。

最后,用一句话总结:"React 18的核心是并发渲染。用useTransition标记非紧急更新,用useDeferredValue延迟值,用Suspense处理加载状态。理解了这些,你的React应用会更流畅。"

愿你的React 18之旅,顺利而愉快。