React 高级特性
本章将介绍 React 中的高级特性,包括 Portals、Error Boundary、Suspense、Strict Mode 以及 Context 的高级用法。这些特性帮助你处理复杂的 UI 场景和边界情况。
1. Portals(传送门)
什么是 Portals?
Portals 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的方案。通常用于模态框、通知提示、悬浮卡片等需要突破父容器限制的场景。
为什么需要 Portals?
考虑这样一个场景:父组件设置了 overflow: hidden 或 z-index,但你需要在视觉上"跳出"父容器显示一个模态框。传统 CSS 方案可能很麻烦,而 Portals 可以轻松解决。
function PortalExample() { const [show, setShow] = useState(false); return ( <div style={{ border: '1px solid #ccc', padding: '20px', position: 'relative', overflow: 'hidden', height: '150px' }}> <p>父容器设置了 <code>overflow: hidden</code> 和固定高度。</p> <button onClick={() => setShow(true)} style={{ padding: '8px 16px', background: '#3b82f6', color: 'white', border: 'none', borderRadius: '4px' }} > 打开 Portal 模态框 </button> {show && createPortal( <div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 9999 }}> <div style={{ background: 'white', padding: '24px', borderRadius: '8px', maxWidth: '400px' }}> <h3 style={{ margin: '0 0 12px 0' }}>这是一个 Portal</h3> <p style={{ margin: '0 0 16px 0', color: '#64748b' }}> 我被渲染到了 document.body 下,不受父容器限制! </p> <button onClick={() => setShow(false)} style={{ padding: '8px 16px', background: '#ef4444', color: 'white', border: 'none', borderRadius: '4px' }} > 关闭 </button> </div> </div>, document.body )} </div> ); }
createPortal 语法
import { createPortal } from 'react-dom';
createPortal(child, container, key?)
参数说明:
child:任何可渲染的 React 子元素container:一个 DOM 元素,作为子元素的容器key(可选):用作 portal 键的唯一字符串或数字
Portal 的事件冒泡
虽然 Portal 渲染在 DOM 树的其他位置,但在 React 组件树中仍然是子组件。这意味着事件会正常冒泡到父组件:
function PortalEventDemo() { const handleParentClick = () => { alert('父组件捕获到了点击事件!'); }; return ( <div onClick={handleParentClick} style={{ padding: '20px', background: '#fef3c7', borderRadius: '8px' }} > <p>点击下面的按钮,事件会冒泡到这里:</p> {createPortal( <button onClick={() => console.log('按钮被点击')} style={{ padding: '8px 16px' }} > Portal 内的按钮 </button>, document.body )} </div> ); }
Portal 的典型应用场景
- 模态框/对话框:突破父容器的
overflow限制 - 全局通知/Toast:固定在视口的某个位置
- 悬浮提示/Tooltip:避免被父容器裁剪
- 下拉菜单:处理复杂的层叠上下文
2. Error Boundary(错误边界)
什么是错误边界?
错误边界是一种 React 组件,它可以捕获子组件树中任何位置的 JavaScript 错误,记录错误,并显示一个备用 UI,而不是让整个组件树崩溃。
为什么需要错误边界?
在 React 中,渲染期间的 JavaScript 错误会导致整个应用崩溃(显示白屏)。错误边界让你可以在组件层级优雅地处理这些错误。
创建错误边界
重要:错误边界必须是类组件,因为只有类组件可以定义 getDerivedStateFromError 或 componentDidCatch 方法。
class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false, error: null }; } static getDerivedStateFromError(error) { // 更新 state,下一次渲染显示备用 UI return { hasError: true, error }; } componentDidCatch(error, errorInfo) { // 可以在这里记录错误日志 console.error('Error caught by boundary:', error, errorInfo); } render() { if (this.state.hasError) { return ( <div style={{ padding: '20px', background: '#fef2f2', borderRadius: '8px', border: '1px solid #fecaca' }}> <h3 style={{ color: '#dc2626', margin: '0 0 8px 0' }}>出错了!</h3> <p style={{ color: '#7f1d1d', margin: 0 }}> {this.props.fallback || '组件渲染发生错误'} </p> <button onClick={() => this.setState({ hasError: false })} style={{ marginTop: '12px', padding: '6px 12px' }} > 重试 </button> </div> ); } return this.props.children; } } function BuggyComponent() { const [crash, setCrash] = useState(false); if (crash) throw new Error("组件崩溃了!"); return ( <button onClick={() => setCrash(true)} style={{ padding: '8px 16px', background: '#ef4444', color: 'white', border: 'none', borderRadius: '4px' }} > 触发渲染错误 </button> ); } function ErrorBoundaryDemo() { return ( <div style={{ padding: '15px' }}> <p style={{ marginBottom: '12px' }}>点击按钮会触发错误,但不会影响其他部分:</p> <ErrorBoundary fallback="糟糕,这个组件崩溃了!"> <BuggyComponent /> </ErrorBoundary> </div> ); }
错误边界的局限性
错误边界无法捕获以下错误:
- 事件处理器中的错误(需要用 try-catch)
- 异步代码中的错误(如 setTimeout、Promise)
- 服务端渲染中的错误
- 错误边界自身抛出的错误
错误边界最佳实践
- 粒度适中:不要在太顶层或太底层使用,建议在关键功能模块外包裹
- 提供有意义的备用 UI:让用户知道发生了什么
- 记录错误:使用
componentDidCatch发送错误到监控服务
class ErrorBoundary extends React.Component {
componentDidCatch(error, errorInfo) {
// 发送到错误监控服务
logErrorToService(error, errorInfo);
}
}
// 使用 Sentry 等服务
componentDidCatch(error, errorInfo) {
Sentry.captureException(error, { extra: errorInfo });
}
3. Suspense(悬念)
什么是 Suspense?
Suspense 让你在组件等待某些操作完成时显示加载状态。它主要用于:
- 代码分割:配合
React.lazy懒加载组件 - 数据获取:配合数据获取库(如 TanStack Query)显示加载状态
代码分割与 React.lazy
React.lazy 让你可以动态导入组件,配合 Suspense 实现代码分割:
// 模拟懒加载组件 const LazyComponent = React.lazy(() => new Promise(resolve => { setTimeout(() => resolve({ default: () => ( <div style={{ padding: '20px', background: '#dcfce7', borderRadius: '8px', textAlign: 'center' }}> <h3 style={{ color: '#166534', margin: '0 0 8px 0' }}>组件加载完成!</h3> <p style={{ margin: 0, color: '#15803d' }}>我是懒加载加载出来的组件</p> </div> ) }), 2000); })); function SuspenseDemo() { return ( <div style={{ padding: '15px' }}> <Suspense fallback={ <div style={{ padding: '20px', background: '#dbeafe', borderRadius: '8px', textAlign: 'center' }}> <div className="animate-pulse">正在加载组件...</div> </div> }> <LazyComponent /> </Suspense> </div> ); }
Suspense 与路由
最常见的用法是配合路由实现页面级别的代码分割:
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// 懒加载页面组件
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<div>加载中...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
Suspense 与数据获取
React 19 引入了 use Hook,可以在组件中直接读取 Promise:
function DataFetcher() { // 模拟数据获取 const dataPromise = new Promise(resolve => { setTimeout(() => resolve({ name: 'React 19', features: ['Actions', 'use Hook', 'Document Metadata'] }), 1500); }); // 使用 React 19 的 use Hook try { const data = use(dataPromise); return ( <div style={{ padding: '15px', background: '#f0fdf4', borderRadius: '8px' }}> <h4>{data.name}</h4> <ul> {data.features.map(f => <li key={f}>{f}</li>)} </ul> </div> ); } catch (e) { if (e instanceof Promise) { throw e; // 抛出 Promise,让 Suspense 捕获 } throw e; } } function SuspenseDataDemo() { return ( <Suspense fallback={<p style={{ color: '#3b82f6' }}>正在获取数据...</p>}> <DataFetcher /> </Suspense> ); }
嵌套 Suspense
可以嵌套多个 Suspense,实现逐步加载:
function App() {
return (
<Suspense fallback={<PageLoader />}>
<Header />
<Suspense fallback={<ContentLoader />}>
<MainContent />
</Suspense>
<Footer />
</Suspense>
);
}
4. Strict Mode(严格模式)
什么是 Strict Mode?
StrictMode 是一个仅在开发环境生效的工具,用于帮助你发现应用中的潜在问题。它不会渲染任何可见 UI。
Strict Mode 检查的内容
- 识别不安全的生命周期方法
- 检测过时的 ref 字符串 API
- 检测过时的 findDOMNode 用法
- 检测过时的 legacy context API
- 检测意外的副作用(通过两次调用某些函数)
使用 Strict Mode
import { StrictMode } from 'react';
function App() {
return (
<StrictMode>
<RootComponent />
</StrictMode>
);
}
// 或在入口文件
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>
);
Strict Mode 的行为
在开发模式下,Strict Mode 会:
- 两次调用组件函数:帮助发现不纯的渲染逻辑
- 两次调用 useEffect:帮助发现没有正确清理的副作用
function StrictModeDemo() { const [count, setCount] = useState(0); useEffect(() => { console.log('Effect 运行'); return () => console.log('Effect 清理'); }, []); // 在 Strict Mode 下,你会看到: // Effect 运行 // Effect 清理 // Effect 运行 return ( <div style={{ padding: '15px', background: '#faf5ff', borderRadius: '8px' }}> <h4 style={{ color: '#7c3aed' }}>Strict Mode 效果</h4> <p>打开控制台查看日志(开发模式下会运行两次)</p> <p>计数:{count}</p> <button onClick={() => setCount(c => c + 1)}>增加</button> </div> ); }
5. Context 高级用法
Context 性能优化
当 Context 值变化时,所有消费该 Context 的组件都会重新渲染。可以通过以下方式优化:
方案一:拆分 Context
// 拆分为独立的 Context const UserContext = createContext(); const ThemeContext = createContext(); function App() { const [user, setUser] = useState({ name: '张三' }); const [theme, setTheme] = useState('light'); return ( <UserContext.Provider value={user}> <ThemeContext.Provider value={theme}> <ChildComponents /> </ThemeContext.Provider> </UserContext.Provider> ); }
方案二:使用 useMemo 缓存值
function ContextOptimizationDemo() { const [state, setState] = useState({ user: '张三', theme: 'light' }); // 使用 useMemo 防止不必要的重新渲染 const value = useMemo(() => ({ state, setUser: (name) => setState(s => ({ ...s, user: name })), setTheme: (theme) => setState(s => ({ ...s, theme })) }), [state]); return ( <div style={{ padding: '15px', background: '#f0f9ff', borderRadius: '8px' }}> <p>用户:{state.user}</p> <p>主题:{state.theme}</p> <div style={{ display: 'flex', gap: '8px' }}> <button onClick={() => value.setUser('李四')}>切换用户</button> <button onClick={() => value.setTheme('dark')}>切换主题</button> </div> </div> ); }
动态 Context
可以根据条件提供不同的 Context 值:
function ThemeProvider({ children }) {
const [isDark, setIsDark] = useState(false);
const theme = useMemo(() => ({
isDark,
colors: isDark
? { bg: '#1e293b', text: '#f8fafc' }
: { bg: '#ffffff', text: '#1e293b' },
toggle: () => setIsDark(d => !d)
}), [isDark]);
return (
<ThemeContext.Provider value={theme}>
{children}
</ThemeContext.Provider>
);
}
嵌套 Provider
当需要多个 Context 时,可以嵌套 Provider:
function AppProviders({ children }) {
return (
<AuthProvider>
<ThemeProvider>
<NotificationProvider>
{children}
</NotificationProvider>
</ThemeProvider>
</AuthProvider>
);
}
6. 高级渲染模式
条件渲染优化
使用 && 运算符时要注意 falsy 值:
function ConditionalRenderDemo() { const [items, setItems] = useState([]); const [count, setCount] = useState(0); return ( <div style={{ padding: '15px', border: '1px solid #e2e8f0', borderRadius: '8px' }}> <h4 style={{ marginBottom: '12px' }}>条件渲染陷阱</h4> {/* 有问题的写法:当 items.length 为 0 时会显示 0 */} <p style={{ color: '#ef4444' }}> 有问题:{items.length && <span>有 {items.length} 个项目</span>} </p> {/* 正确的写法 */} <p style={{ color: '#10b981' }}> 正确:{items.length > 0 && <span>有 {items.length} 个项目</span>} </p> {/* 或使用三元运算符 */} <p> 推荐:{items.length > 0 ? `有 ${items.length} 个项目` : '没有项目'} </p> <button onClick={() => setItems([1, 2, 3])} style={{ marginTop: '8px' }}> 添加项目 </button> </div> ); }
Fragment 最佳实践
使用 Fragment 可以减少不必要的 DOM 嵌套:
function FragmentDemo() { const items = [ { id: 1, term: 'React', definition: '用于构建用户界面的 JavaScript 库' }, { id: 2, term: 'Vue', definition: '渐进式 JavaScript 框架' }, { id: 3, term: 'Angular', definition: '企业级 Web 应用框架' } ]; return ( <dl style={{ padding: '15px', background: '#f8fafc', borderRadius: '8px' }}> {items.map(item => ( <Fragment key={item.id}> <dt style={{ fontWeight: 'bold', color: '#1e40af', marginTop: '12px' }}> {item.term} </dt> <dd style={{ color: '#64748b', marginLeft: '0' }}> {item.definition} </dd> </Fragment> ))} </dl> ); }
小结
本章我们学习了 React 的高级特性:
Portals
- 将组件渲染到 DOM 树的其他位置
- 事件冒泡仍然遵循 React 组件树
- 适用于模态框、通知、悬浮提示等
Error Boundary
- 捕获子组件树中的渲染错误
- 必须是类组件
- 无法捕获事件处理器和异步代码中的错误
Suspense
- 处理异步操作的加载状态
- 配合
React.lazy实现代码分割 - 支持嵌套使用
Strict Mode
- 仅在开发环境生效
- 帮助发现潜在问题和不安全的 API
- 会两次调用某些函数以检测副作用
Context 高级用法
- 拆分 Context 减少不必要的重渲染
- 使用 useMemo 缓存 Context 值
- 嵌套多个 Provider
练习
- 使用 Portals 实现一个全局通知组件,支持显示成功/错误/警告消息
- 创建一个错误边界组件,支持自定义备用 UI 和错误上报功能
- 使用 Suspense 和 React.lazy 实现路由级别的代码分割
- 创建一个主题切换功能,使用 Context 并优化性能
- 在项目中启用 Strict Mode,检查并修复发现的问题