跳到主要内容

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>
  );
}
结果
Loading...

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>
  );
}
结果
Loading...

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>
  );
}
结果
Loading...

错误边界的局限性​

错误边界无法捕获以下错误:

  • 事件处理器中的错误(需要用 try-catch)
  • 异步代码中的错误(如 setTimeout、Promise)
  • 服务端渲染中的错误
  • 错误边界自身抛出的错误

错误边界最佳实践​

  1. 粒度适中:不要在太顶层或太底层使用,建议在关键功能模块外包裹
  2. 提供有意义的备用 UI:让用户知道发生了什么
  3. 记录错误:使用 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>
  );
}
结果
Loading...

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>
  );
}
结果
Loading...

嵌套 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 检查的内容​

  1. 识别不安全的生命周期方法
  2. 检测过时的 ref 字符串 API
  3. 检测过时的 findDOMNode 用法
  4. 检测过时的 legacy context API
  5. 检测意外的副作用(通过两次调用某些函数)

使用 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>
  );
}
结果
Loading...

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>
  );
}
结果
Loading...

方案二:使用 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>
  );
}
结果
Loading...

动态 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>
  );
}
结果
Loading...

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>
  );
}
结果
Loading...

小结​

本章我们学习了 React 的高级特性:

Portals

  • 将组件渲染到 DOM 树的其他位置
  • 事件冒泡仍然遵循 React 组件树
  • 适用于模态框、通知、悬浮提示等

Error Boundary

  • 捕获子组件树中的渲染错误
  • 必须是类组件
  • 无法捕获事件处理器和异步代码中的错误

Suspense

  • 处理异步操作的加载状态
  • 配合 React.lazy 实现代码分割
  • 支持嵌套使用

Strict Mode

  • 仅在开发环境生效
  • 帮助发现潜在问题和不安全的 API
  • 会两次调用某些函数以检测副作用

Context 高级用法

  • 拆分 Context 减少不必要的重渲染
  • 使用 useMemo 缓存 Context 值
  • 嵌套多个 Provider

练习​

  1. 使用 Portals 实现一个全局通知组件,支持显示成功/错误/警告消息
  2. 创建一个错误边界组件,支持自定义备用 UI 和错误上报功能
  3. 使用 Suspense 和 React.lazy 实现路由级别的代码分割
  4. 创建一个主题切换功能,使用 Context 并优化性能
  5. 在项目中启用 Strict Mode,检查并修复发现的问题

参考资源​