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的区别
| 对比项 | useTransition | useDeferredValue |
|---|---|---|
| 针对的对象 | 更新(函数) | 值 |
| 控制方式 | 主动标记更新 | 被动延迟值 |
| 适用场景 | 你能控制更新 | 你不能控制更新来源 |
| 加载状态 | 有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之旅,顺利而愉快。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录