React 进阶
Context
Context 提供了一种在组件树中传递数据的方式,无需通过 props 逐层手动传递。它解决了"prop drilling"问题,让深层嵌套的组件也能访问到全局性的数据(如主题、语言偏好、用户认证信息等)。
createContext 与 Provider
createContext 用于创建一个 Context 对象,其返回值包含 Provider 和 Consumer 两个组件。Provider 接收一个 value 属性,传递给其下所有子组件。
import { createContext, useContext } from 'react';
// 创建 Context,可传入默认值
const ThemeContext = createContext('light');
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}当 Provider 的 value 发生变化时,所有消费该 Context 的组件都会重新渲染。注意,即使 Consumer 只使用了 value 中的部分字段,只要引用变化就会触发重渲染。
useContext
useContext 是消费 Context 的推荐方式,比 Context.Consumer 组件更加简洁直观:
function ThemedButton() {
const theme = useContext(ThemeContext);
return <button className={theme}>按钮</button>;
}useContext 接收一个 Context 对象,返回该 Context 的当前值。当 Provider 的 value 更新时,useContext 会自动触发组件重新渲染。
性能考量
Context 的 value 发生变化时,所有消费该 Context 的组件都会重新渲染,无论它们是否真正依赖变化的部分。为优化性能,可以采取以下策略:
拆分 Context:将频繁变化的值与稳定不变的值放入不同的 Context。
使用 useMemo 包裹 value:
function App({ children }) {
const [theme, setTheme] = useState('light');
const value = useMemo(() => ({ theme, setTheme }), [theme]);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}- 组件隔离:将消费 Context 的组件与渲染开销大的子组件分离,避免不必要的子组件重渲染。
Context 嵌套
实际应用中,经常需要多个 Context 嵌套使用。React 允许多层 Provider 嵌套,每个 Consumer 会向上查找最近的 Provider:
<ThemeContext.Provider value="dark">
<UserContext.Provider value={{ name: 'Alice' }}>
<LocaleContext.Provider value="zh-CN">
<App />
</LocaleContext.Provider>
</UserContext.Provider>
</ThemeContext.Provider>多层嵌套在提供便利的同时也可能导致组件树结构臃肿。可以考虑使用组合模式或将多个值合并到一个 Context 中,减少嵌套层级。
Portals
Portals 允许将子节点渲染到父组件 DOM 层级之外的 DOM 节点中。这在处理模态框、弹出层、工具提示等场景时尤为有用。
createPortal 基础用法
createPortal 接收两个参数:要渲染的 React 子元素和目标 DOM 节点。
import { createPortal } from 'react-dom';
function Modal({ children, isOpen }) {
if (!isOpen) return null;
return createPortal(
<div className="modal-overlay">
<div className="modal-content">{children}</div>
</div>,
document.body
);
}事件冒泡
尽管 Portal 的内容在 DOM 树中位于不同位置,但事件冒泡遵循 React 组件树的层次结构,而非 DOM 树结构。这意味着从 Portal 内部触发的事件可以冒泡到父组件,让父组件能够捕获 Portal 内的事件:
function Container() {
return (
<div onClick={() => alert('点击捕获')}>
<Modal isOpen={true}>
<button>点击我</button> {/* 点击此按钮会触发 Container 的 onClick */}
</Modal>
</div>
);
}这一特性使得 Portal 在模态框中非常实用——父组件仍然可以通过事件冒泡感知到子组件内部的操作。
模态框与弹出层
Portals 最常见的应用场景是模态框。将模态框渲染到 document.body 下可以避免 CSS 溢出隐藏、z-index 层级等问题:
function ModalPortal({ isOpen, onClose, children }) {
useEffect(() => {
const handleEsc = (e) => e.key === 'Escape' && onClose();
if (isOpen) document.addEventListener('keydown', handleEsc);
return () => document.removeEventListener('keydown', handleEsc);
}, [isOpen, onClose]);
if (!isOpen) return null;
return createPortal(
<div className="modal-backdrop" onClick={onClose}>
<div className="modal-body" onClick={(e) => e.stopPropagation()}>
{children}
</div>
</div>,
document.body
);
}其他常见用法包括:下拉菜单、弹出提示、通知栏、全屏加载遮罩等。
Error Boundary
Error Boundary(错误边界)是 React 组件,用于捕获其子组件树中发生的 JavaScript 错误,记录错误并显示降级 UI,而非让整个应用崩溃。
错误边界的工作原理
错误边界通过实现类组件的生命周期方法 componentDidCatch 和静态方法 getDerivedStateFromError 来工作:
getDerivedStateFromError:渲染阶段调用,用于更新状态以显示降级 UI。componentDidCatch:提交阶段调用,用于执行副作用(如错误日志上报)。
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error('捕获到错误:', error, errorInfo);
// 可在这里将错误信息上报到监控系统
}
render() {
if (this.state.hasError) {
return <h1>出现了一些问题,请稍后重试。</h1>;
}
return this.props.children;
}
}静态 getDerivedStateFromError
在 React 18 中,getDerivedStateFromError 被标记为静态方法。它接收抛出的错误对象作为参数,必须返回一个对象来更新组件的 state。当使用静态 getDerivedStateFromError 时,务必注意:
- 它是静态方法,无法访问组件实例(无
this)。 - 它只用于更新 state 以触发降级 UI 渲染。
- 不要在此方法中执行副作用(移到
componentDidCatch中)。
错误边界的限制
错误边界并非万能,有以下限制需注意:
- 仅捕获子组件树中的错误:无法捕获自身错误、事件处理器中的错误、异步代码中的错误以及服务端渲染中的错误。
- 事件处理器中的错误:需要使用
try-catch自行处理。 - 异步错误:例如
setTimeout、Promise中的错误不会被子组件的错误边界捕获。 - 每个错误边界只捕获一次:如果降级 UI 本身也报错,则错误会冒泡到上层错误边界。
对于事件处理中的错误,可以在 onClick 等事件处理函数中使用 try-catch:
function SafeButton() {
const handleClick = () => {
try {
// 可能抛出错误的操作
riskyOperation();
} catch (error) {
console.error('事件处理错误:', error);
}
};
return <button onClick={handleClick}>安全按钮</button>;
}Suspense
Suspense 是 React 提供的一种声明式机制,用于处理组件等待异步操作(如数据获取、代码分割)时的加载状态。
React.lazy 与代码分割
React.lazy 允许动态导入组件,实现代码分割。使用 Suspense 包裹懒加载组件,在加载过程中显示 fallback 内容:
import { lazy, Suspense } from 'react';
const LazyDashboard = lazy(() => import('./Dashboard'));
function App() {
return (
<Suspense fallback={<div>加载中...</div>}>
<LazyDashboard />
</Suspense>
);
}代码分割可以显著减少初始 bundle 大小,提升首屏加载性能。配合 Webpack 或 Vite 的代码分割能力,每个懒加载的组件会生成独立的 chunk。
Suspense 与数据获取
在 React 18+ 中,Suspense 也用于数据获取场景。结合支持 Suspense 的数据请求库(如 React Query、Relay、SWR 等),可以声明式地处理加载状态:
function UserProfile() {
const user = useSuspenseUserData(userId);
return <div>{user.name}</div>;
}
function Page() {
return (
<Suspense fallback={<UserSkeleton />}>
<UserProfile />
</Suspense>
);
}使用 Suspense 进行数据获取有以下优势:
- 声明式加载状态:无需在组件内手动管理 loading 状态。
- 自动协调:多个使用 Suspense 的子组件可以协调加载状态的展示。
- 渐进式加载:结合流式渲染实现内容的逐步呈现。
嵌套 Suspense
多个 Suspense 可以嵌套使用,实现更细粒度的加载控制。每个 Suspense 边界独立管理其 fallback,已加载的内容立即展示:
<Suspense fallback={<PageSkeleton />}>
<Layout>
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
<Suspense fallback={<ContentSkeleton />}>
<MainContent />
</Suspense>
</Layout>
</Suspense>嵌套 Suspense 使得页面可以逐步加载——先展示已准备好的部分,而不必等待所有内容就绪。
Suspense 的 fallback 策略
Suspense 的 fallback 展示遵循以下策略:
- 首次挂载时,Suspense 会立即展示 fallback。
- 当子组件进入 Suspense 状态时,如果已经可见,React 会延迟展示 fallback(避免闪烁)。
- 如果延迟时间超过 Suspense 配置的超时时间(在 React 18 中由 startTransition 控制),则显示 fallback。
Concurrent Mode
React 18 引入了 Concurrent Mode(并发模式),赋予 React 同时准备多个 UI 版本的能力。这是 React 渲染引擎的一次重大架构升级。
可中断渲染
在传统同步渲染中,一旦开始渲染就无法中断,可能导致主线程长时间阻塞。并发模式下的渲染是可中断的——React 可以在渲染过程中暂停、恢复或放弃渲染任务:
- 高优先级任务(如用户输入)可以打断低优先级任务(如列表渲染)。
- 渲染在内存中执行,不会阻塞主线程与用户交互。
- React 在适当时机提交已完成的高优先级更新,保证界面的响应性。
优先级调度
React 18 引入了基于优先级的调度系统,不同类型的更新拥有不同的优先级:
- 事件更新:最高优先级,如点击、输入。
- 过渡更新:较低优先级,如页面切换、数据过滤。
- 空闲更新:最低优先级,如预加载、数据预取。
import { useTransition } from 'react';
function SearchPage() {
const [isPending, startTransition] = useTransition();
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const handleChange = (e) => {
// 立即响应用户输入
setQuery(e.target.value);
// 将搜索结果更新标记为过渡,可被中断
startTransition(() => {
setResults(filterExpensiveData(e.target.value));
});
};
return (
<div>
<input value={query} onChange={handleChange} />
{isPending && <Spinner />}
<ResultsList data={results} />
</div>
);
}Transition
useTransition 和 startTransition 是 React 18 引入的核心 API,用于区分紧急更新和非紧急更新。过渡更新有以下特点:
- 可以被更高优先级的更新中断。
- 正在进行的过渡可以通过
isPending获取状态。 - 旧 UI 会保持可见直到新 UI 准备就绪。
startTransition 也可以在非 Hook 环境中使用:
import { startTransition } from 'react';
startTransition(() => {
setSearchResults(filterData(input));
});useDeferredValue
useDeferredValue 允许延迟更新 UI 的某个部分,使其"落后于"其余部分。它适用于希望保持 UI 响应,但需要处理大量数据的场景:
import { useDeferredValue } from 'react';
function SearchPage({ query }) {
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
const suggestions = useMemo(() => {
return expensiveFilter(deferredQuery);
}, [deferredQuery]);
return (
<div>
<input defaultValue={query} />
{isStale && <div>正在更新...</div>}
<SuggestionsList data={suggestions} />
</div>
);
}与 useTransition 的区别在于:
useTransition控制状态更新的时机。useDeferredValue控制值的展示时机(适用于受控于父组件的 props)。
React 18 并发特性详解
React 18 引入了一系列并发特性,以下逐一深入解析。
Automatic Batching
React 18 扩展了自动批处理的范围。在之前版本中,只有 React 事件处理器中的状态更新会被批处理;现在,setTimeout、原生事件处理器、Promise 回调中的更新也会自动批处理:
// React 18 中,两次 setState 会被合并为一次渲染
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
}, 1000);如需退出批处理,可使用 flushSync:
import { flushSync } from 'react-dom';
flushSync(() => {
setCount(c => c + 1);
});
// DOM 已在此处同步更新
flushSync(() => {
setFlag(f => !f);
});useId
useId 是一个新的 Hook,用于生成唯一 ID,在客户端和服务端生成一致的 ID,消除 hydration 不匹配问题:
import { useId } from 'react';
function FormField() {
const id = useId();
return (
<div>
<label htmlFor={id}>名称</label>
<input id={id} type="text" />
</div>
);
}useSyncExternalStore
专为第三方状态管理库设计的 Hook,用于在并发模式下安全地读取外部 store,确保一致性:
import { useSyncExternalStore } from 'react';
function useStoreSnapshot(store) {
const state = useSyncExternalStore(
store.subscribe,
() => store.getSnapshot()
);
return state;
}useInsertionEffect
useInsertionEffect 专为 CSS-in-JS 库设计,允许在 DOM 变更前插入样式规则。它比 useLayoutEffect 更早触发,确保样式在组件渲染前已注入。
Server-Side Rendering 增强
React 18 的 SSR 进行了全面重构:
- 流式 HTML:使用
renderToPipeableStream支持流式输出 HTML,服务端逐步发送渲染内容。 - 选择性 Hydration:根据用户交互优先级,有选择地 hydrate 页面部分,无需等待所有 JS 加载完毕。
- Suspense 在服务端:服务端支持 Suspense,可以延迟发送 fallback 内容,待数据就绪后再流式补充。
import { renderToPipeableStream } from 'react-dom/server';
app.get('/', (req, res) => {
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
res.setHeader('content-type', 'text/html');
pipe(res);
},
});
});并发模式的使用策略
并发模式是 React 18 的底层架构升级,大部分现有代码无需修改即可受益。建议采用渐进式策略引入并发特性:
- 升级到 React 18,享受自动批处理带来的性能提升。
- 在路由级别的页面切换中使用
startTransition,优化导航体验。 - 在搜索、过滤等场景中使用
useDeferredValue或useTransition。 - 为大型组件或页面使用
React.lazy配合 Suspense 进行代码分割。 - 结合流式 SSR 和 Suspense 实现首屏性能优化。
实际应用场景代码示例
场景一:多主题 Context + Portal 通知
结合 Context 和 Portals 实现一个具备主题感知能力的全局通知系统:
const NotificationContext = createContext();
function NotificationProvider({ children }) {
const [notifications, setNotifications] = useState([]);
const addNotification = useCallback((msg) => {
const id = Date.now();
setNotifications(prev => [...prev, { id, msg }]);
setTimeout(() => {
setNotifications(prev => prev.filter(n => n.id !== id));
}, 3000);
}, []);
return (
<NotificationContext.Provider value={addNotification}>
{children}
{createPortal(
<div className="notification-container">
{notifications.map(n => (
<div key={n.id} className="notification-item">{n.msg}</div>
))}
</div>,
document.body
)}
</NotificationContext.Provider>
);
}
function useNotify() {
return useContext(NotificationContext);
}场景二:异步加载与错误边界组合
结合 Suspense、React.lazy 和 Error Boundary 实现健壮的异步加载方案:
const Dashboard = lazy(() => import('./Dashboard'));
const Analytics = lazy(() => import('./Analytics'));
function DashboardPage() {
return (
<ErrorBoundary fallback={<DashboardErrorFallback />}>
<Suspense fallback={<DashboardSkeleton />}>
<Dashboard />
</Suspense>
</ErrorBoundary>
);
}
function AnalyticsPage() {
return (
<ErrorBoundary fallback={<AnalyticsErrorFallback />}>
<Suspense fallback={<AnalyticsSkeleton />}>
<Analytics />
</Suspense>
</ErrorBoundary>
);
}每个页面模块拥有独立的错误边界和加载状态,互不影响,确保单个模块的异常不会扩散到整个应用。
场景三:并发模式下的搜索体验
综合使用 useTransition 和 useDeferredValue 优化搜索体验:
function SearchApp() {
const [inputValue, setInputValue] = useState('');
const [searchQuery, setSearchQuery] = useState('');
const [isPending, startTransition] = useTransition();
const deferredSearchQuery = useDeferredValue(searchQuery);
const handleInput = (e) => {
const value = e.target.value;
setInputValue(value);
startTransition(() => {
setSearchQuery(value);
});
};
const results = useMemo(() => {
return queryDatabase(deferredSearchQuery);
}, [deferredSearchQuery]);
return (
<div className="search-container">
<input
type="text"
value={inputValue}
onChange={handleInput}
placeholder="搜索..."
className="search-input"
/>
{isPending && <div className="search-hint">更新中...</div>}
<div className="results-list" style={{ opacity: isPending ? 0.7 : 1 }}>
{results.map(item => <SearchResult key={item.id} item={item} />)}
</div>
</div>
);
}该模式确保用户输入始终即时响应,搜索结果的更新在后台进行,不会阻塞输入操作,同时通过视觉反馈让用户感知到搜索状态。
场景四:嵌套 Suspense 实现渐进式页面加载
利用嵌套 Suspense 边界,让页面内容按优先级逐步呈现:
function ProductPage({ productId }) {
return (
<ErrorBoundary fallback={<PageErrorFallback />}>
<Suspense fallback={<PageSkeleton />}>
<ProductHeader productId={productId} />
<div className="product-layout">
<Suspense fallback={<ImageSkeleton />}>
<ProductGallery productId={productId} />
</Suspense>
<div className="product-info">
<Suspense fallback={<DetailsSkeleton />}>
<ProductDetails productId={productId} />
</Suspense>
<Suspense fallback={<ReviewsSkeleton />}>
<ProductReviews productId={productId} />
</Suspense>
</div>
</div>
</Suspense>
</ErrorBoundary>
);
}页面头部和基础布局先展示骨架屏,图片、详情、评论各自独立加载——谁先准备好谁先渲染,用户不必等待全部内容就绪即可开始交互。