跳到主要内容

React 高级特性

本章将介绍 React 中的高级特性,包括 Portals、Error Boundary、Suspense、Strict Mode 以及 Context 的高级用法。这些特性帮助你处理复杂的 UI 场景和边界情况。


1. Portals(传送门)

什么是 Portals?

Portals 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的方案。通常用于模态框、通知提示、悬浮卡片等需要突破父容器限制的场景。

为什么需要 Portals?

考虑这样一个场景:父组件设置了 overflow: hiddenz-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 错误会导致整个应用崩溃(显示白屏)。错误边界让你可以在组件层级优雅地处理这些错误。

创建错误边界

重要:错误边界必须是类组件,因为只有类组件可以定义 getDerivedStateFromErrorcomponentDidCatch 方法。

实时编辑器
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,检查并修复发现的问题

参考资源