Skip to content

React 速查表

用于使用组件构建用户界面的 JavaScript 库。

01

JSX 与组件

函数组件

React 组件是返回 JSX 的 JavaScript 函数。组件名称必须以大写字母开头(小写 = HTML 标签)。Props 作为属性传递并在参数中解构。JSX 是 React.createElement() 的语法糖。始终返回单个根元素(或使用 Fragments)。组件必须是纯的——相同 props = 相同输出。

react
// Basic function component
function Welcome({ name }) {
  return <h1>Hello, {name}!</h1>;
}

// Arrow function component
const Greeting = ({ name = 'Guest' }) => (
  <p>Welcome, {name}!</p>
);

// Composing components
function App() {
  return (
    <div>
      <Welcome name="Alice" />
      <Welcome name="Bob" />
      <Greeting />
    </div>
  );
}

JSX 表达式

JSX 允许在花括号 {} 中嵌入 JavaScript 表达式。可以放置变量、函数调用、三元运算符和任何返回值的表达式。语句(if、for、switch)不能直接使用——使用三元或 IIFE。布尔值(true、false)、null 和 undefined 不渲染任何内容。数字和字符串渲染为文本。对象不是有效的 React 子元素。

react
function Expression({ user, items }) {
  const fullName = user.first + ' ' + user.last;
  const itemCount = items.length;

  return (
    <div>
      {/* Expressions in curly braces */}
      <h1>{fullName}</h1>
      <p>{itemCount} items</p>

      {/* Conditional */}
      <p>{itemCount > 0 ? 'In stock' : 'Sold out'}</p>

      {/* Method calls */}
      <p>{fullName.toUpperCase()}</p>

      {/* Numbers and booleans render as nothing */}
      <p>{false}{null}{undefined}</p>
    </div>
  );
}

Fragments 与 JSX 列表

Fragments(<>...</>)将多个元素分组而不添加额外 DOM 节点——比包裹在 <div> 中更干净。需要传递 key 时使用 <Fragment key={...}>。JSX 元素数组需要唯一的 key 属性。虽然数组索引作为 key 对静态列表有效,但对动态列表使用稳定 ID 以防止渲染错误。Fragments 通过减少不必要的包裹元素提高性能。

react
// Fragment: group without extra DOM node
function App() {
  return (
    <>
      <header>Header</header>
      <main>Content</main>
      <footer>Footer</footer>
    </>
  );
}

// Array of elements (needs keys)
function List() {
  const fruits = ['Apple', 'Banana', 'Cherry'];
  return (
    <ul>
      {fruits.map((fruit, i) => (
        <li key={i}>{fruit}</li>
      ))}
    </ul>
  );
}

条件渲染

React 提供多种条件渲染模式。提前返回用于 if/else 逻辑。三元(cond ? A : B)用于二选一。逻辑与(cond && <Component/>)用于显示/隐藏。IIFE 用于复杂分支。避免在 JSX 中嵌入复杂逻辑——提取到变量或辅助函数。对于 switch 语句,使用查找对象或提取到单独函数。假值(0、'')会渲染,因此对数字使用三元而非 &&。

react
function Greeting({ isLoggedIn, user }) {
  // 1. If/else (use early return)
  if (!isLoggedIn) return <Login />;

  // 2. Ternary operator
  return (
    <div>
      {user ? <Dashboard user={user} /> : <Loading />}

      {/* 3. Logical AND (render if truthy) */}
      {user.isAdmin && <AdminPanel />}

      {/* 4. IIFE for complex logic */}
      {(() => {
        if (user.role === 'admin') return <Admin />;
        if (user.role === 'mod') return <Mod />;
        return <User />;
      })()}
    </div>
  );
}

Children 与 Render Props

children prop 包含开标签和闭标签之间的元素——对于可组合组件(卡片、模态框、布局)至关重要。Render props 将函数作为 prop 传递,接收数据并返回 JSX——是 HOC 和 hooks 共享逻辑的替代方案。虽然 render props 在 hooks 出现后不太常见,但对于组件注入模式仍然有用。children 是不需要显式传递的特殊 prop。

react
// children prop: content between tags
function Card({ title, children }) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div className="card-body">{children}</div>
    </div>
  );
}

// Usage
<Card title="Profile">
  <p>Name: Alice</p>
  <p>Age: 30</p>
</Card>

// Render prop pattern
function DataProvider({ render }) {
  const data = fetchData();
  return <div>{render(data)}</div>;
}
02

Props

传递 Props

Props 是从父组件传递到子组件的只读数据。可以是任何 JavaScript 值:字符串、数字、布尔值、数组、对象或函数。字符串值使用引号(name='Alice'),所有其他值使用花括号(age={30})。函数作为 props 实现子到父通信(回调)。Props 向下流动——子组件不能修改 props。对于双向数据绑定,将状态提升到共同父组件。

react
// Parent passes props to child
function App() {
  return (
    <User
      name="Alice"
      age={30}
      isActive={true}
      tags={['admin', 'dev']}
      onClick={() => console.log('clicked')}
    />
  );
}

// Child receives props
function User({ name, age, isActive, tags, onClick }) {
  return (
    <div onClick={onClick}>
      <h1>{name}</h1>
      <p>Age: {age}</p>
      <p>Status: {isActive ? 'Active' : 'Inactive'}</p>
    </div>
  );
}

默认与可选 Props

默认 prop 值通过解构设置(param = defaultValue)。如果未传递 prop,则为 undefined。使用短路(bio && <p>)或三元来条件渲染可选 props。PropTypes(传统)或 TypeScript 接口可在开发时验证 prop 类型。defaultProps(类组件)对函数组件已弃用——使用解构默认值代替。

react
// Default values via destructuring
function Button({ color = 'blue', size = 'md', children }) {
  return (
    <button className={'btn btn-' + color + ' btn-' + size}>
      {children}
    </button>
  );
}

// Optional props (undefined if not passed)
function Profile({ name, bio }) {
  return (
    <div>
      <h1>{name}</h1>
      {bio && <p>{bio}</p>}
    </div>
  );
}

// Usage
<Button>Click</Button>           {/* color='blue', size='md' */}
<Profile name="Alice" />         {/* bio is undefined */}

展开与剩余 Props

展开运算符(...props)将所有 props 传递给子元素——对包裹组件(HOC、样式化组件)有用。剩余运算符在解构特定 props 后收集剩余 props。此模式在设计系统中很常见,包裹组件将未知 props 转发给 DOM 元素。注意:展开可能覆盖显式属性——顺序很重要({...props} className='x' vs className='x' {...props})。

react
// Spread: pass all props to child
function Input(props) {
  return <input {...props} className="input" />;
}

// Usage
<Input type="text" placeholder="Name" value="Alice" />

// Rest: collect remaining props
function Button({ label, ...rest }) {
  return <button {...rest}>{label}</button>;
}

// Selective spreading
function Card({ title, children, ...divProps }) {
  return (
    <div {...divProps}>
      <h2>{title}</h2>
      {children}
    </div>
  );
}

Prop 类型与 TypeScript

TypeScript 接口为 props 提供编译时类型检查——新 React 项目的推荐方法。可选 props 使用 ?(isActive?: boolean)。PropTypes 提供运行时验证(仅在开发中),对没有 TypeScript 的 JavaScript 项目有用。isRequired 确保 prop 已提供。TypeScript 在运行前捕获类型错误,对大型代码库更优。

react
// TypeScript interface (recommended)
interface UserProps {
  name: string;
  age: number;
  isActive?: boolean;       // optional
  onClick: (id: number) => void;
}

function User({ name, age, isActive = true, onClick }: UserProps) {
  return <div onClick={() => onClick(1)}>{name}, {age}</div>;
}

// PropTypes (runtime checking, legacy)
import PropTypes from 'prop-types';
User.propTypes = {
  name: PropTypes.string.isRequired,
  age: PropTypes.number,
  isActive: PropTypes.bool,
};

Prop 逐层传递与 Context

Prop 逐层传递(prop drilling)发生在 props 通过不使用它们的多个组件层时。对于 2-3 层是可以接受的。对于更深的树,使用 Context API、状态管理库(Redux、Zustand)或组件组合。组合(将组件作为 props 或 children 传递)通常比 Context 更优雅地解决传递问题。问问自己:每个中间组件都需要这些数据吗?如果不需要,重新考虑组件结构。

react
// Prop drilling: passing through multiple levels
function App() {
  const [user, setUser] = useState(null);
  return <Layout user={user} />;
}

function Layout({ user }) {
  return <Sidebar user={user} />;
}

function Sidebar({ user }) {
  return <UserInfo user={user} />;
}

// Solution: Context API (see State Management section)
// Avoid drilling more than 2-3 levels
03

useState 与状态

基础 useState

useState 是向函数组件添加状态的基础 hook。它返回一个数组:[currentValue, setterFunction]。初始值可以是任何类型。调用 setter 触发使用新值的重新渲染。状态更新是异步的——调用 setCount 后值不会立即改变。每个组件实例都有自己的独立状态。setter 是稳定的(跨渲染保持相同引用)。

react
import { useState } from 'react';

function Counter() {
  // [currentValue, setterFunction] = useState(initialValue)
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={() => setCount(count - 1)}>-1</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}

函数式更新

当新状态依赖于前一个状态时,使用函数式更新:setCount(prev => prev + 1)。这保证使用最新状态,即使多个更新被批处理。没有函数式更新,快速连续调用可能使用过期状态。React 18 自动批处理状态更新(即使在 promise 和 timeout 中),因此函数式更新对正确性至关重要。

react
function Counter() {
  const [count, setCount] = useState(0);

  // BAD: may not work correctly with rapid updates
  const incrementBad = () => setCount(count + 1);

  // GOOD: functional update uses previous state
  const incrementGood = () => setCount(prev => prev + 1);

  // Batch updates
  const addThree = () => {
    setCount(prev => prev + 1);
    setCount(prev => prev + 1);
    setCount(prev => prev + 1);
  };

  return <button onClick={addThree}>Count: {count}</button>;
}

对象与数组状态

切勿直接修改状态——始终创建新对象/数组。对于对象,使用展开运算符复制现有属性:{...prev, [field]: value}。对于数组,使用展开添加([...prev, newItem])、filter 删除、map 更新。React 比较引用来检测变化——被修改的对象具有相同引用,因此 React 不会重新渲染。这是初学者 React bug 的头号来源。

react
function Form() {
  const [form, setForm] = useState({ name: '', email: '', age: 0 });

  // BAD: mutates state directly
  // form.name = 'Alice'; setForm(form);

  // GOOD: spread to create new object
  const updateField = (field, value) => {
    setForm(prev => ({ ...prev, [field]: value }));
  };

  return (
    <input
      value={form.name}
      onChange={e => updateField('name', e.target.value)}
    />
  );
}

// Array state
function TodoList() {
  const [todos, setTodos] = useState([]);
  const addTodo = (text) => setTodos(prev => [...prev, { id: Date.now(), text }]);
  const removeTodo = (id) => setTodos(prev => prev.filter(t => t.id !== id));
}

惰性初始状态

如果初始状态需要昂贵计算,向 useState 传递函数(惰性初始化)。该函数仅在首次渲染时运行,而非每次重新渲染。这对解析 localStorage、从 IndexedDB 获取或任何 CPU 密集型设置很重要。函数形式:useState(() => initialValue)。对于简单值(数字、字符串),直接传递值即可——惰性初始化不必要。

react
import { useState } from 'react';

function ExpensiveInit() {
  // BAD: runs on every render (even though result is ignored)
  const [data, setData] = useState(computeExpensiveValue());

  // GOOD: lazy initialization — function runs only once
  const [data2, setData2] = useState(() => computeExpensiveValue());

  // Reading from localStorage
  const [user, setUser] = useState(() => {
    const saved = localStorage.getItem('user');
    return saved ? JSON.parse(saved) : null;
  });

  return <div>{data2}</div>;
}

多个状态变量

对独立值使用多个 useState 调用而非一个大对象。这使更新更简单(无需展开)并防止不必要的重新渲染。将相关值分组到单个状态对象中(例如表单字段)。对于具有多个子值的复杂状态逻辑,考虑使用 useReducer。经验法则:如果状态更新独立,使用单独的 useState;如果相关/相互依赖,使用 useReducer 或单个对象。

react
function LoginForm() {
  // Multiple independent state variables
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [errors, setErrors] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [rememberMe, setRememberMe] = useState(false);

  const handleSubmit = (e) => {
    e.preventDefault();
    setIsSubmitting(true);
    // ... validation and submission
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={email} onChange={e => setEmail(e.target.value)} />
      <input type="password" value={password}
        onChange={e => setPassword(e.target.value)} />
      <button disabled={isSubmitting}>Submit</button>
    </form>
  );
}
04

useEffect 与副作用

基础 useEffect

useEffect 在渲染后执行副作用。effect 函数在组件绘制后运行。清理函数(返回的)在下一次 effect 之前和卸载时运行——对清除计时器、订阅和监听器至关重要。依赖数组控制 effect 何时重新运行:[] = 挂载时一次,[dep] = dep 变化时,无数组 = 每次渲染。始终清理以防止内存泄漏。

react
import { useState, useEffect } from 'react';

function Timer() {
  const [seconds, setSeconds] = useState(0);

  // Runs after every render
  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);

    // Cleanup function runs before next effect or unmount
    return () => clearInterval(interval);
  }, []); // empty array = run once on mount

  return <p>Seconds: {seconds}</p>;
}

依赖数组

依赖数组对 useEffect 行为至关重要。空数组 [] = 仅挂载(类似 componentDidMount)。带依赖 [a, b] = 挂载时和 a 或 b 变化时运行。无数组 = 每次渲染(很少是你想要的)。缺少依赖导致过期闭包。包含不必要依赖导致过度重新运行。使用 exhaustive-deps ESLint 规则捕获错误。effect 中使用的组件作用域的每个值都应在 deps 中。

react
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  // Runs once on mount (empty deps)
  useEffect(() => {
    console.log('Component mounted');
  }, []);

  // Runs when userId changes
  useEffect(() => {
    fetch('/api/users/' + userId)
      .then(r => r.json())
      .then(setUser);
  }, [userId]); // re-run when userId changes

  // Runs on every render (no deps) - rarely needed
  useEffect(() => {
    console.log('Every render');
  });

  return <div>{user?.name}</div>;
}

清理与订阅

清理对订阅、事件监听器、计时器和 WebSocket 连接至关重要。没有清理,会导致内存泄漏和重复处理程序。清理函数运行:(1) 在下一次 effect 重新运行之前,(2) 组件卸载时。对于 WebSocket/事件监听器,始终在清理中移除它们。对于依赖于 effect 的状态,在清理中重置以避免显示前一个 roomId 的过期数据。

react
function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    const ws = new WebSocket('wss://chat.example.com/' + roomId);

    ws.onmessage = (event) => {
      setMessages(prev => [...prev, JSON.parse(event.data)]);
    };

    // Cleanup: close connection when roomId changes or unmount
    return () => {
      ws.close();
      setMessages([]); // reset for new room
    };
  }, [roomId]);

  // Window event listener
  useEffect(() => {
    const handleResize = () => console.log(window.innerWidth);
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return <div>{messages.length} messages</div>;
}

获取数据

useEffect 中的数据获取需要取消标志以防止卸载后设置状态(导致 React 警告)。'cancelled' 标志确保 setUsers/setError/setLoading 仅在组件仍挂载时运行。对于生产应用,考虑使用数据获取库(React Query、SWR),它们自动处理缓存、去重和竞态条件。空依赖数组 [] 确保获取仅在挂载时发生一次。

react
function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;

    const fetchData = async () => {
      try {
        setLoading(true);
        const res = await fetch('/api/users');
        const data = await res.json();
        if (!cancelled) setUsers(data);
      } catch (err) {
        if (!cancelled) setError(err.message);
      } finally {
        if (!cancelled) setLoading(false);
      }
    };

    fetchData();
    return () => { cancelled = true; };
  }, []);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;
  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

useLayoutEffect 与 useEffect

useEffect 在浏览器绘制后异步运行——如果测量 DOM 元素,用户可能看到短暂闪烁。useLayoutEffect 在 DOM 变更后但在绘制前同步运行——防止视觉闪烁。对于影响布局的 DOM 测量(getBoundingClientRect、滚动位置)使用 useLayoutEffect。其他所有情况使用 useEffect(它不阻塞绘制)。在服务器上,useLayoutEffect 会警告——使用 useIsomorphicLayoutEffect 模式。

react
import { useState, useEffect, useLayoutEffect } from 'react';

function Tooltip({ text }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  // useEffect: runs AFTER paint (user may see flash)
  useEffect(() => {
    const rect = document.getElementById('tip').getBoundingClientRect();
    setPosition({ x: rect.x, y: rect.y });
  }, [text]);

  // useLayoutEffect: runs BEFORE paint (no flash)
  useLayoutEffect(() => {
    const rect = document.getElementById('tip').getBoundingClientRect();
    setPosition({ x: rect.x, y: rect.y });
  }, [text]);

  return <div id="tip" style={{ left: position.x, top: position.y }}>{text}</div>;
}
05

useRef、useMemo 与 useCallback

useRef 基础

useRef 返回一个跨渲染持久化的可变对象 { current: value }。与状态不同,更改 ref.current 不会触发重新渲染。常见用途:(1) 访问 DOM 元素(通过 ref 属性),(2) 存储不影响渲染的可变值(计时器、前一个值),(3) 存储最新值供回调使用。ref 对象跨渲染具有相同标识。初始值传递给 useRef(initialValue)。

react
import { useRef } from 'react';

function FocusInput() {
  // ref to access DOM element
  const inputRef = useRef(null);

  const focus = () => inputRef.current.focus();
  const clear = () => {
    inputRef.current.value = '';
    inputRef.current.focus();
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={focus}>Focus</button>
      <button onClick={clear}>Clear</button>
    </div>
  );
}

useRef 用于可变值

useRef 存储跨渲染持久化的可变值而不触发重新渲染。这非常适合:计时器 ID、WebSocket 引用、跟踪前一个状态和计数渲染次数。由于更改 ref.current 不会导致重新渲染,更改它时 UI 不会更新——对于应影响 UI 的值使用状态。渲染计数模式(ref.current++)对调试有用但不应在生产逻辑中使用。

react
function Stopwatch() {
  const [seconds, setSeconds] = useState(0);
  const intervalRef = useRef(null);
  const renderCount = useRef(0);

  // Track render count (doesn't trigger re-render)
  renderCount.current++;

  const start = () => {
    if (intervalRef.current) return;
    intervalRef.current = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);
  };

  const stop = () => {
    clearInterval(intervalRef.current);
    intervalRef.current = null;
  };

  return (
    <div>
      <p>{seconds}s (render #{renderCount.current})</p>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </div>
  );
}

useMemo

useMemo 记忆化(缓存)计算值,仅在依赖变化时重新计算。用于昂贵计算(过滤、排序、复杂数学)以避免每次渲染都重新运行。依赖数组的工作方式类似 useEffect。过度使用可能损害性能(记忆化有其自身开销)——仅记忆化真正昂贵的操作。useMemo 也用于保留对象引用以防止子组件重新渲染。

react
import { useState, useMemo } from 'react';

function ProductList({ products, filter }) {
  const [search, setSearch] = useState('');

  // Memoize expensive computation
  const filtered = useMemo(() => {
    console.log('Filtering...');
    return products
      .filter(p => p.category === filter)
      .filter(p => p.name.includes(search));
  }, [products, filter, search]); // recompute only when these change

  // Memoize a value
  const totalPrice = useMemo(() =>
    filtered.reduce((sum, p) => sum + p.price, 0),
  [filtered]);

  return (
    <div>
      <input value={search} onChange={e => setSearch(e.target.value)} />
      <p>Total: ${totalPrice}</p>
      {filtered.map(p => <div key={p.id}>{p.name}</div>)}
    </div>
  );
}

useCallback

useCallback 记忆化函数,除非依赖变化否则跨渲染返回相同引用。这防止记忆化子组件(包裹在 memo() 中)的不必要重新渲染。没有 useCallback,每次父组件渲染都创建新函数引用,导致 memo() 子组件重新渲染。将回调传递给优化的子组件时使用 useCallback。与 useMemo 一样,不要过度使用——仅用于作为 props 传递给记忆化子组件的函数。

react
import { useState, useCallback, memo } from 'react';

// Memoized child component
const Button = memo(function Button({ onClick, label }) {
  console.log('Button rendered');
  return <button onClick={onClick}>{label}</button>;
});

function App() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');

  // Without useCallback: new function every render
  // const handleClick = () => setCount(c => c + 1);

  // With useCallback: stable function reference
  const handleClick = useCallback(() => {
    setCount(c => c + 1);
  }, []); // empty deps = stable forever

  return (
    <div>
      <input value={text} onChange={e => setText(e.target.value)} />
      <Button onClick={handleClick} label="Click" />
      <p>Count: {count}</p>
    </div>
  );
}

转发 Refs

forwardRef 允许父组件将 ref 传递给子组件的 DOM 元素。useImperativeHandle 自定义 ref 暴露的内容——而非 DOM 节点,可以暴露特定方法(focus、clear、getValue)。这对于创建具有命令式 API 的可重用输入组件很有用。React 19 简化了 refs(ref 现在是常规 prop),但库仍需要 forwardRef。避免过度使用命令式句柄——优先使用声明式 props。

react
import { useRef, forwardRef, useImperativeHandle } from 'react';

// forwardRef: pass ref to child component
const FancyInput = forwardRef(function FancyInput(props, ref) {
  return <input ref={ref} className="fancy" {...props} />;
});

// useImperativeHandle: expose specific methods
const CustomInput = forwardRef(function CustomInput(props, ref) {
  const inputRef = useRef(null);

  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus(),
    clear: () => { inputRef.current.value = ''; },
    getValue: () => inputRef.current.value,
  }));

  return <input ref={inputRef} />;
});

// Usage
function App() {
  const ref = useRef(null);
  return (
    <>
      <CustomInput ref={ref} />
      <button onClick={() => ref.current.focus()}>Focus</button>
      <button onClick={() => ref.current.clear()}>Clear</button>
    </>
  );
}
06

useReducer 与 Context

useReducer 基础

useReducer 是复杂状态逻辑的 useState 替代方案。reducer 是纯函数:(state, action) => newState。Action 描述发生了什么;reducer 决定如何更新状态。此模式使状态转换可预测且可测试。dispatch 是稳定的(相同引用)。始终返回新状态对象(切勿修改)。默认情况应为未知 action 抛出错误。当状态有多个子值或下一状态依赖复杂逻辑时使用 useReducer。

react
import { useReducer } from 'react';

// Reducer function: (state, action) => newState
function counterReducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return { count: 0 };
    case 'set':
      return { count: action.payload };
    default:
      throw new Error('Unknown action: ' + action.type);
  }
}

function Counter() {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
      <button onClick={() => dispatch({ type: 'set', payload: 10 })}>Set 10</button>
    </div>
  );
}

复杂 Reducer

复杂 reducer 管理多个相关的状态片段。每个 action 类型处理特定状态转换。始终展开前一个状态({...state})以保留无关字段。对于嵌套更新(如切换 todo),使用 map 创建带更新项的新数组。reducer 必须是纯的——无副作用、无 API 调用。将 reducer 提取到单独文件以提高可测试性。对于非常复杂的状态,考虑 Redux Toolkit 等库。

react
const initialState = {
  todos: [],
  filter: 'all',
  loading: false,
};

function todoReducer(state, action) {
  switch (action.type) {
    case 'add':
      return { ...state, todos: [...state.todos, action.todo] };
    case 'toggle':
      return {
        ...state,
        todos: state.todos.map(t =>
          t.id === action.id ? { ...t, done: !t.done } : t
        ),
      };
    case 'delete':
      return { ...state, todos: state.todos.filter(t => t.id !== action.id) };
    case 'set_filter':
      return { ...state, filter: action.filter };
    case 'set_loading':
      return { ...state, loading: action.loading };
    default:
      return state;
  }
}

function TodoApp() {
  const [state, dispatch] = useReducer(todoReducer, initialState);
  // dispatch({ type: 'add', todo: { id: 1, text: 'Learn React', done: false } })
}

Context API

Context API 无需 prop 逐层传递即可跨组件树共享状态。使用 createContext(defaultValue) 创建。用带 value prop 的 Provider 包裹消费者。使用 useContext(Context) 消费。Context 值变化触发所有消费者重新渲染。为提高性能,拆分 context(ThemeContext、UserContext),使组件仅在其特定 context 变化时重新渲染。当没有 Provider 包裹消费者时使用默认值。

react
import { createContext, useContext, useState } from 'react';

// 1. Create context with default value
const ThemeContext = createContext('light');
const UserContext = createContext(null);

// 2. Provider component
function App() {
  const [theme, setTheme] = useState('light');
  const [user, setUser] = useState(null);

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <UserContext.Provider value={{ user, setUser }}>
        <Page />
      </UserContext.Provider>
    </ThemeContext.Provider>
  );
}

// 3. Consume context
function Page() {
  const { theme } = useContext(ThemeContext);
  const { user } = useContext(UserContext);
  return <div className={'page theme-' + theme}>Hello {user?.name}</div>;
}

Context 与 Reducer

将 Context 与 useReducer 结合创建轻量级全局状态管理系统(迷你 Redux)。Provider 暴露 state 和 dispatch。自定义 hook(useStore)在 Provider 外使用时提供错误处理。此模式非常适合中型应用。对于频繁更新的超大型应用,考虑拆分 context 或使用 Redux/Zustand 以避免每次状态变化都重新渲染所有消费者。

react
import { createContext, useContext, useReducer } from 'react';

// Combine Context + useReducer for global state
const StoreContext = createContext(null);

function StoreProvider({ children }) {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <StoreContext.Provider value={{ state, dispatch }}>
      {children}
    </StoreContext.Provider>
  );
}

// Custom hook for easy consumption
function useStore() {
  const context = useContext(StoreContext);
  if (!context) throw new Error('useStore must be used within StoreProvider');
  return context;
}

// Usage
function Component() {
  const { state, dispatch } = useStore();
  return <button onClick={() => dispatch({ type: 'action' })}>Click</button>;
}

useContext 性能

当 context 值变化时,所有消费者都重新渲染——即使它们仅使用值的一小部分。优化方法:(1) 拆分 context 使组件仅订阅所需内容。(2) 使用 useMemo 记忆化 context 值以防止值实际未变化时的重新渲染。(3) 使用选择器(use-context-selector 库)进行细粒度订阅。对于高频更新(如鼠标位置),Context 可能导致性能问题——考虑 refs 或外部存储。

react
// SPLIT contexts for performance
const ThemeContext = createContext();
const UserContext = createContext();
const CartContext = createContext();

// Each provider manages its own state
function App() {
  return (
    <ThemeProvider>
      <UserProvider>
        <CartProvider>
          <App />
        </CartProvider>
      </UserProvider>
    </ThemeProvider>
  );
}

// Component only re-renders when its context changes
function ThemedButton() {
  const { theme } = useContext(ThemeContext);
  // Won't re-render when user or cart changes
  return <button className={theme}>Button</button>;
}

// Memoize context value to prevent unnecessary re-renders
function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  const value = useMemo(() => ({ user, setUser }), [user]);
  return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}
07

事件与表单

事件处理

React 事件使用 camelCase(onClick,而非 onclick)。事件对象是 SyntheticEvent(原生事件的包裹)。e.preventDefault() 阻止默认行为(表单提交、链接导航)。e.stopPropagation() 阻止事件冒泡。要向处理程序传递参数,使用箭头函数:onClick={() => handleDelete(id)}。避免内联定义复杂处理程序——提取以提高可读性。React 事件是池化的(17 之前),如需异步访问请调用 e.persist()。

react
function App() {
  // Click event
  const handleClick = (e) => {
    e.preventDefault();
    console.log('Button clicked', e.target);
  };

  // With parameters (use arrow function)
  const handleDelete = (id) => {
    console.log('Delete item', id);
  };

  return (
    <div>
      <button onClick={handleClick}>Click</button>
      <button onClick={() => handleDelete(42)}>Delete</button>
      <div onMouseEnter={() => console.log('hover')}
           onMouseLeave={() => console.log('leave')}>
        Hover me
      </div>
    </div>
  );
}

受控输入

受控输入的值由 React 状态控制。value prop 设置输入值,onChange 更新状态。这使 React 成为表单数据的'单一数据源'。每次按键都触发状态更新和重新渲染。对于复杂表单,这可能冗长——考虑 React Hook Form 或 Formik 等库。受控输入实现实时验证和动态行为。始终将 onChange 与 value(或 readOnly)一起使用以避免 React 警告。

react
function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('Email:', email, 'Password:', password);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={e => setEmail(e.target.value)}
        placeholder="Email"
      />
      <input
        type="password"
        value={password}
        onChange={e => setPassword(e.target.value)}
        placeholder="Password"
      />
      <button type="submit">Login</button>
    </form>
  );
}

多字段表单

对于多字段表单,使用单个状态对象和通用 handleChange 函数。每个输入上的 name 属性匹配状态键。处理程序使用计算属性名([name]: value)更新正确字段。对于复选框,使用 checked 而非 value。此模式显著减少样板代码。对于文件输入,使用非受控输入(它们无法完全受控)。对于带验证的复杂表单考虑 React Hook Form。

react
function RegistrationForm() {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    password: '',
    country: 'us',
    agree: false,
  });

  const handleChange = (e) => {
    const { name, value, type, checked } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: type === 'checkbox' ? checked : value,
    }));
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log(formData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="username" value={formData.username} onChange={handleChange} />
      <input name="email" type="email" value={formData.email} onChange={handleChange} />
      <select name="country" value={formData.country} onChange={handleChange}>
        <option value="us">USA</option>
        <option value="uk">UK</option>
      </select>
      <label>
        <input type="checkbox" name="agree" checked={formData.agree} onChange={handleChange} />
        Agree to terms
      </label>
      <button type="submit">Register</button>
    </form>
  );
}

非受控输入

非受控输入使用 ref 直接访问 DOM 值,无需 React 状态。defaultValue prop 设置初始值(非 value)。对于不需要实时验证或动态行为的表单更简单。文件输入必须是非受控的(其值出于安全原因是只读的)。非受控输入也适用于与非 React 代码集成。权衡:无法轻松实时验证或转换输入。大多数情况下优先使用受控输入。

react
import { useRef } from 'react';

function UncontrolledForm() {
  const emailRef = useRef(null);
  const passwordRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('Email:', emailRef.current.value);
    console.log('Password:', passwordRef.current.value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input ref={emailRef} type="email" defaultValue="" />
      <input ref={passwordRef} type="password" defaultValue="" />
      <button type="submit">Submit</button>
    </form>
  );
}

// File input (must be uncontrolled)
function FileUpload() {
  const fileRef = useRef(null);
  return <input ref={fileRef} type="file" />;
}

验证与错误处理

表单验证可在提交时或每次变化时进行。validate 函数返回 errors 对象——空表示有效。在每个字段旁边条件显示错误。为获得更好用户体验,在 blur 时(用户离开字段后)验证而非每次按键。React Hook Form + Zod 或 Formik + Yup 等库提供强大的验证模式、错误管理和 touch/blur 跟踪。始终也在服务器上验证——客户端验证是为了用户体验而非安全。

react
function ValidatedForm() {
  const [values, setValues] = useState({ email: '', password: '' });
  const [errors, setErrors] = useState({});

  const validate = () => {
    const errs = {};
    if (!values.email) errs.email = 'Email is required';
    else if (!/\S+@\S+\.\S+/.test(values.email)) errs.email = 'Invalid email';
    if (!values.password) errs.password = 'Password is required';
    else if (values.password.length < 8) errs.password = 'Min 8 characters';
    return errs;
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    const errs = validate();
    setErrors(errs);
    if (Object.keys(errs).length === 0) {
      console.log('Form valid', values);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={values.email}
        onChange={e => setValues(v => ({ ...v, email: e.target.value }))} />
      {errors.email && <span className="error">{errors.email}</span>}
      <input type="password" value={values.password}
        onChange={e => setValues(v => ({ ...v, password: e.target.value }))} />
      {errors.password && <span className="error">{errors.password}</span>}
      <button type="submit">Submit</button>
    </form>
  );
}
08

列表与条件渲染

渲染列表

使用 .map() 将数组转换为 JSX 元素。每个元素需要唯一 key prop——使用稳定 ID(todo.id),而非数组索引。key 帮助 React 识别哪些项变化(添加、删除、重排)以进行高效 DOM 更新。使用索引作为 key 在列表项重排或在开头插入时会导致 bug。对于空列表,渲染回退消息。对于过滤/排序列表考虑 useMemo 以避免每次渲染都重新计算。

react
function TodoList({ todos }) {
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          <span>{todo.text}</span>
          <button onClick={() => toggle(todo.id)}>
            {todo.done ? 'Undo' : 'Done'}
          </button>
        </li>
      ))}
    </ul>
  );
}

// Filtering and sorting
function FilteredList({ items, filter }) {
  const visible = items
    .filter(item => item.category === filter)
    .sort((a, b) => a.name.localeCompare(b.name));

  return (
    <ul>
      {visible.map(item => <li key={item.id}>{item.name}</li>)}
    </ul>
  );
}

Key 详解

key 必须在兄弟元素(相同父元素)中唯一,但可在不同列表中重复。key 帮助 React 的协调算法:当 key 变化时,React 销毁并重建组件(丢失状态)。使用索引 key 时,在开头插入项会移动所有索引,导致 React 重新渲染所有内容。使用稳定 ID key 时,React 仅渲染新项。key 不需要全局唯一——只需在列表内唯一。不要使用随机 key(Math.random())——它们每次渲染都变化。

react
// GOOD: stable, unique keys
{todos.map(todo => (
  <TodoItem key={todo.id} todo={todo} />
))}

// BAD: index as key (causes bugs with reordering)
{todos.map((todo, index) => (
  <TodoItem key={index} todo={todo} />
))}

// When index keys are OK:
// - Static list (never reordered/filtered)
// - List items have no state
// - List is never prepended to

// Key must be unique among siblings
function List() {
  return (
    <div>
      {users.map(u => <User key={u.id} user={u} />)}
      {posts.map(p => <Post key={p.id} post={p} />)}
      {/* IDs can repeat across different lists */}
    </div>
  );
}

条件渲染模式

存在多种条件渲染模式。提前返回对守卫子句最干净(加载、错误、认证)。元素变量适用于组件中间的 if/else。三元(cond ? A : B)用于 JSX 中的二选一。逻辑与(cond && <X/>)用于显示/隐藏。对象查找用于类似 switch 的行为。避免嵌套三元——提取到变量或组件。对于数字,使用三元而非 &&(0 && <X/> 渲染 0)。

react
function UserDashboard({ user, loading, error }) {
  // 1. Early returns for loading/error states
  if (loading) return <Spinner />;
  if (error) return <ErrorMessage error={error} />;
  if (!user) return <Login />;

  // 2. Element variables
  let greeting;
  if (user.isAdmin) {
    greeting = <h1>Welcome Admin {user.name}</h1>;
  } else {
    greeting = <h1>Welcome {user.name}</h1>;
  }

  return (
    <div>
      {greeting}

      {/* 3. Ternary for either/or */}
      {user.hasNotifications ? <NotificationBadge /> : null}

      {/* 4. && for show/hide */}
      {user.isAdmin && <AdminPanel />}

      {/* 5. Switch via object lookup */}
      {{ free: <FreePlan />, pro: <ProPlan />, enterprise: <EnterprisePlan /> }
        [user.plan]}
    </div>
  );
}

列表过滤与搜索

可搜索/可过滤列表结合 useState 用于过滤和 useMemo 用于性能。过滤函数检查查询和类别。始终处理空状态(无结果)。对于大型列表(1000+ 项),考虑虚拟化(react-window、react-virtualized)仅渲染可见项。为 API 调用防抖搜索输入。不区分大小写的搜索使用 toLowerCase()。对于复杂过滤,提取到单独函数或自定义 hook。

react
function SearchableList({ items }) {
  const [query, setQuery] = useState('');
  const [category, setCategory] = useState('all');

  // Memoize filtered results
  const filtered = useMemo(() => {
    return items.filter(item => {
      const matchesQuery = item.name.toLowerCase().includes(query.toLowerCase());
      const matchesCategory = category === 'all' || item.category === category;
      return matchesQuery && matchesCategory;
    });
  }, [items, query, category]);

  return (
    <div>
      <input
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="Search..."
      />
      <select value={category} onChange={e => setCategory(e.target.value)}>
        <option value="all">All</option>
        <option value="food">Food</option>
        <option value="tech">Tech</option>
      </select>

      {filtered.length === 0 ? (
        <p>No results found</p>
      ) : (
        <ul>
          {filtered.map(item => <li key={item.id}>{item.name}</li>)}
        </ul>
      )}
    </div>
  );
}

动态组件

动态组件渲染使用查找对象将类型映射到组件。这在 CMS 驱动的内容、表单构建器和页面构建器中很常见。组件映射避免了长 switch/if 链。始终用回退组件处理未知类型。展开 props({...block.props})将所有属性传递给动态组件。此模式灵活且可扩展——添加新块类型只需添加到映射中。将变量大写(Component)使 JSX 将其视为组件。

react
// Render different components based on type
const componentMap = {
  text: TextBlock,
  image: ImageBlock,
  video: VideoBlock,
  quote: QuoteBlock,
};

function ContentRenderer({ blocks }) {
  return (
    <div>
      {blocks.map(block => {
        const Component = componentMap[block.type];
        if (!Component) return <UnknownBlock key={block.id} type={block.type} />;

        return <Component key={block.id} {...block.props} />;
      })}
    </div>
  );
}

// Usage
const blocks = [
  { id: 1, type: 'text', props: { content: 'Hello' } },
  { id: 2, type: 'image', props: { src: 'pic.jpg', alt: 'Picture' } },
];
09

性能优化

React.memo

React.memo 包裹组件以在 props 未变化时防止重新渲染(浅比较)。用于经常以相同 props 渲染的组件。第二个参数是自定义比较函数:返回 true 跳过重新渲染,false 重新渲染。memo 仅在组件渲染昂贵或是频繁渲染父组件的子组件时有用。不要包裹每个组件——记忆化有开销。结合 useCallback/useMemo 获得最大效果。

react
import { memo } from 'react';

// Memoized component: only re-renders if props change
const ExpensiveCard = memo(function ExpensiveCard({ title, content }) {
  console.log('Card rendered');
  return (
    <div className="card">
      <h2>{title}</h2>
      <p>{content}</p>
    </div>
  );
});

// Custom comparison function
const MyComponent = memo(function MyComponent(props) {
  return <div>{props.value}</div>;
}, (prevProps, nextProps) => {
  // Return true if props are equal (skip re-render)
  return prevProps.value === nextProps.value;
});

代码分割

代码分割通过按需加载组件来减少初始包大小。React.lazy + Suspense 启用动态导入。fallback prop 在组件加载时显示。基于路由的分割(惰性加载页面组件)影响最大。基于组件的分割适用于不需要立即使用的重型组件(图表、编辑器)。每个惰性导入创建单独的 chunk。React.lazy 用于默认导出;对于命名导出,包裹在模块中。

react
import { lazy, Suspense } from 'react';

// Lazy load component (code-split)
const AdminPanel = lazy(() => import('./AdminPanel'));
const Dashboard = lazy(() => import('./Dashboard'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Router>
        <Route path="/admin" element={<AdminPanel />} />
        <Route path="/dashboard" element={<Dashboard />} />
      </Router>
    </Suspense>
  );
}

// Route-based splitting (most common)
// Component-based splitting
const HeavyChart = lazy(() => import('./HeavyChart'));

function Page({ showChart }) {
  return (
    <div>
      {showChart && (
        <Suspense fallback={<Spinner />}>
          <HeavyChart />
        </Suspense>
      )}
    </div>
  );
}

长列表虚拟化

虚拟化仅渲染长列表中的可见项,显著提高性能。react-window 和 react-virtualized 是流行的库。仅渲染约 12 个(可见的)而非 10,000 个 DOM 节点,带有可滚动容器。这减少了 DOM 大小和渲染时间。对 100+ 项的列表使用虚拟化。权衡:更复杂的实现,搜索/查找的潜在问题(项不在 DOM 中)。可变高度列表需要 VariableSizeList。

react
import { FixedSizeList } from 'react-window';

// Virtualized list: only renders visible items
function BigList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      {items[index].name}
    </div>
  );

  return (
    <FixedSizeList
      height={600}
      width="100%"
      itemCount={items.length}
      itemSize={50}
    >
      {Row}
    </FixedSizeList>
  );
}

// Without virtualization: rendering 10,000 items is slow
// With virtualization: only ~12 visible items are rendered

防抖与节流

防抖延迟执行直到活动暂停(例如用户停止输入)。节流将执行限制为每个间隔一次。两者都防止过多的 API 调用或计算。useDebounce hook 仅在用户停止输入指定延迟后更新防抖值。这对搜索输入、调整大小处理程序和滚动事件至关重要。对于节流,使用 lodash.throttle 等库或用时间戳实现。始终在 useEffect 中清理计时器。

react
import { useState, useEffect } from 'react';

// Debounce hook: delays execution until user stops typing
function useDebounce(value, delay) {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debounced;
}

function SearchInput() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 300);

  // API call only fires when user stops typing for 300ms
  useEffect(() => {
    if (debouncedQuery) {
      fetch('/api/search?q=' + debouncedQuery);
    }
  }, [debouncedQuery]);

  return <input value={query} onChange={e => setQuery(e.target.value)} />;
}

性能分析与优化

Profiler 组件测量渲染时间。phase 是 'mount' 或 'update'。actualDuration 是以毫秒为单位的渲染时间。使用 React DevTools Profiler 获取可视化火焰图。优化前,先分析找到实际瓶颈——不要猜测。常见性能问题:(1) 不必要的重新渲染(用 memo/useMemo/useCallback 修复),(2) 昂贵计算(用 useMemo 修复),(3) 大型列表(用虚拟化修复),(4) 大包(用代码分割修复)。过早优化浪费时间——先测量。

react
import { Profiler } from 'react';

function App() {
  const onRender = (id, phase, actualDuration) => {
    console.log(id + ' ' + phase + ': ' + actualDuration + 'ms');
  };

  return (
    <Profiler id="App" onRender={onRender}>
      <ExpensiveComponent />
    </Profiler>
  );
}

/* Optimization checklist:
1. React.memo for expensive components
2. useMemo for expensive calculations
3. useCallback for props passed to memoized children
4. Code splitting (lazy) for routes
5. Virtualization for long lists
6. Debounce rapid events (search, resize)
7. Avoid inline objects/functions as props
8. Use keys correctly in lists
9. Profile with React DevTools Profiler
10. Check unnecessary re-renders with why-did-you-render
*/
10

模式与错误边界

自定义 Hooks

自定义 hook 将可重用的有状态逻辑提取到以 'use' 为前缀的函数中。它们可以调用其他 hook。自定义 hook 是在组件之间共享逻辑的主要方式(替代 HOC 和 render props)。对于多个值返回对象,对于单个值返回值/数组。始终处理加载和错误状态。取消模式(cancelled 标志)防止卸载后状态更新。以 'use' 前缀命名 hook 以使 ESLint 规则工作。

react
import { useState, useEffect } from 'react';

// Reusable data fetching hook
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;
    setLoading(true);
    fetch(url)
      .then(res => {
        if (!res.ok) throw new Error('HTTP ' + res.status);
        return res.json();
      })
      .then(data => { if (!cancelled) { setData(data); setError(null); }})
      .catch(err => { if (!cancelled) setError(err.message); })
      .finally(() => { if (!cancelled) setLoading(false); });
    return () => { cancelled = true; };
  }, [url]);

  return { data, loading, error };
}

// Usage
function UserProfile({ id }) {
  const { data: user, loading, error } = useFetch('/api/users/' + id);
  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;
  return <h1>{user.name}</h1>;
}

useLocalStorage Hook

useLocalStorage 将状态持久化到 localStorage。惰性初始化器在挂载时从 localStorage 读取。useEffect 在值变化时写入 localStorage。try/catch 处理配额超出错误和 JSON 解析错误(数据损坏)。此模式适用于任何持久状态:主题、用户偏好、草稿内容。对于 SSR 兼容性,检查 typeof window !== 'undefined'。对于跨标签页同步,监听 'storage' 事件。类似 hook:useSessionStorage、useCookie。

react
import { useState, useEffect } from 'react';

function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    try {
      const saved = localStorage.getItem(key);
      return saved ? JSON.parse(saved) : initialValue;
    } catch {
      return initialValue;
    }
  });

  useEffect(() => {
    try {
      localStorage.setItem(key, JSON.stringify(value));
    } catch (e) {
      console.error('LocalStorage error:', e);
    }
  }, [key, value]);

  return [value, setValue];
}

// Usage
function ThemeToggle() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  return (
    <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
      Current: {theme}
    </button>
  );
}

错误边界

错误边界捕获子组件渲染/生命周期方法中的错误,防止整个应用崩溃。它们必须是类组件(尚无 hook 等价物)。getDerivedStateFromError 更新状态以显示回退 UI。componentDidCatch 记录错误(发送到 Sentry、LogRocket 等)。错误边界不捕获:事件处理程序、异步代码、setTimeout、边界本身中的错误。包裹特定部分以隔离故障。'重试' 按钮重置错误状态。

react
import { Component } from 'react';

class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    console.error('Caught error:', error, errorInfo);
    // Send to error reporting service
    // logErrorToService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        this.props.fallback || (
          <div>
            <h1>Something went wrong.</h1>
            <p>{this.state.error?.message}</p>
            <button onClick={() => this.setState({ hasError: false })}>
              Try again
            </button>
          </div>
        )
      );
    }
    return this.props.children;
  }
}

// Usage: wrap components
<ErrorBoundary fallback={<ErrorPage />}>
  <App />
</ErrorBoundary>

高阶组件(HOC)

高阶组件(HOC)是接受组件并返回增强组件的函数。它们是 hooks 出现之前共享逻辑的主要模式。常见用途:认证、加载状态、主题化。HOC 可能导致'包裹地狱'(深度嵌套组件)和 prop 冲突。对于新代码,优先使用自定义 hook——它们更简单、更可组合,且不增加组件树。HOC 仍适用于类组件或与需要它们的库集成时。

react
// HOC: function that takes a component and returns a new one
function withLoading(Component) {
  return function WithLoading({ isLoading, ...props }) {
    if (isLoading) return <div>Loading...</div>;
    return <Component {...props} />;
  };
}

// HOC for authentication
function withAuth(Component) {
  return function WithAuth(props) {
    const { user } = useContext(AuthContext);
    if (!user) return <Redirect to="/login" />;
    return <Component {...props} user={user} />;
  };
}

// Usage
const UserList = withLoading(withAuth(BaseUserList));

// Note: Prefer hooks over HOCs for new code
// HOCs are mainly for class components or library compatibility

复合组件

复合组件让用户从简单部分组合复杂组件。父组件(Select)提供 context,子组件(Trigger、Options、Option)消费它。此模式被 Radix UI、Headless UI 和 React Aria 等库使用。优点:灵活的 API(用户可重排/省略部分),通过 context 隐式状态共享,干净的 JSX。组件作为静态属性附加(Select.Trigger)。这是高级模式——用于可重用 UI 库,而非一次性组件。

react
// Compound components: components that work together
function Select({ children, value, onChange }) {
  const [isOpen, setIsOpen] = useState(false);
  const context = { value, onChange, isOpen, setIsOpen };

  return (
    <SelectContext.Provider value={context}>
      <div className="select">{children}</div>
    </SelectContext.Provider>
  );
}

Select.Trigger = function Trigger({ children }) {
  const { isOpen, setIsOpen } = useContext(SelectContext);
  return <button onClick={() => setIsOpen(!isOpen)}>{children}</button>;
};

Select.Options = function Options({ children }) {
  const { isOpen } = useContext(SelectContext);
  return isOpen ? <div className="options">{children}</div> : null;
};

Select.Option = function Option({ value, children }) {
  const { onChange, setIsOpen } = useContext(SelectContext);
  return (
    <div onClick={() => { onChange(value); setIsOpen(false); }}>
      {children}
    </div>
  );
}

// Usage: clean, declarative API
<Select value={val} onChange={setVal}>
  <Select.Trigger>Choose...</Select.Trigger>
  <Select.Options>
    <Select.Option value="a">Option A</Select.Option>
    <Select.Option value="b">Option B</Select.Option>
  </Select.Options>
</Select>
11

自定义 Hooks

useFetch Hook

自定义 hook 将可重用的有状态逻辑提取到以 'use' 为前缀的函数中。useFetch 封装带加载/错误状态的数据获取。AbortController 在组件卸载或 URL 变化时取消进行中的请求(防止竞态条件和内存泄漏)。始终在 useEffect 中为异步操作包含清理。自定义 hook 可以调用其他 hook(useState、useEffect、useContext)。它们是在不使用 render props 或 HOC 的情况下在组件间共享逻辑的主要方式。以 'use' 前缀命名使 React 的 rules-of-hooks linter 工作。

react
function useFetch(url, options = {}) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let abortController = new AbortController();
    setLoading(true);
    fetch(url, { ...options, signal: abortController.signal })
      .then((res) => {
        if (!res.ok) throw new Error(res.statusText);
        return res.json();
      })
      .then((data) => { setData(data); setError(null); })
      .catch((err) => {
        if (err.name !== "AbortError") setError(err.message);
      })
      .finally(() => setLoading(false));
    return () => abortController.abort();
  }, [url]);

  return { data, loading, error };
}

// Usage
function Profile({ userId }) {
  const { data, loading, error } = useFetch(`/api/users/${userId}`);
  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;
  return <div>{data.name}</div>;
}

useLocalStorage Hook

useLocalStorage 将 React 状态与 localStorage 同步。惰性初始化器仅在首次渲染时从 localStorage 读取。useEffect 在值变化时写入 localStorage。try/catch 处理 localStorage 已满或禁用(隐私浏览)的情况。此 hook 使持久状态像 useState 一样简单。对于跨标签页同步,添加 storage 事件监听器。对于 SSR 安全,保护 window 访问。此模式也适用于 sessionStorage——只需交换 API。

react
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    try {
      const stored = window.localStorage.getItem(key);
      return stored ? JSON.parse(stored) : initialValue;
    } catch {
      return initialValue;
    }
  });

  useEffect(() => {
    try {
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch (e) {
      console.error("LocalStorage write failed:", e);
    }
  }, [key, value]);

  return [value, setValue];
}

// Usage: persists state across page reloads
function Settings() {
  const [theme, setTheme] = useLocalStorage("theme", "light");
  const [fontSize, setFontSize] = useLocalStorage("fontSize", 14);
  return (
    <div>
      <button onClick={() => setTheme("dark")}>Dark</button>
      <button onClick={() => setTheme("light")}>Light</button>
    </div>
  );
}

useDebounce Hook

useDebounce 延迟更新值直到用户停止输入指定延迟。这对搜索输入、自动保存和由用户输入触发的 API 调用至关重要——它防止每次按键时的过多调用。清理函数在值在延迟到期前再次变化时清除超时。防抖值仅在暂停后更新,触发下游效果(如 API 调用)频率较低。对于带尾随调用的立即执行,改用 useThrottle。结合 useFetch 实现高效的即输即搜。

react
function useDebounce(value, delay = 500) {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);
  return debounced;
}

// Usage: debounce search input
function Search() {
  const [query, setQuery] = useState("");
  const debouncedQuery = useDebounce(query, 300);

  useEffect(() => {
    if (debouncedQuery) {
      fetch(`/api/search?q=${debouncedQuery}`)
        .then((res) => res.json())
        .then(setResults);
    }
  }, [debouncedQuery]);

  return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}

usePrevious Hook

usePrevious 利用 useEffect 在渲染后运行的事实——ref.current 在渲染期间仍持有旧值,然后更新为新值。这是比较当前和前一个状态的常见模式。useWindowSize 通过 resize 监听器跟踪视口尺寸。始终在 useEffect 返回中清理事件监听器以防止内存泄漏。这些实用 hook 演示了自定义 hook 如何封装与 DOM 相关的逻辑,使组件更干净且逻辑可重用和可测试。

react
function usePrevious(value) {
  const ref = useRef(null);
  useEffect(() => {
    ref.current = value; // update AFTER render
  }, [value]);
  return ref.current; // returns previous value during render
}

// Usage: compare current vs previous
function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);

  return (
    <div>
      <p>Now: {count}, before: {prevCount}</p>
      {count > prevCount && <p>Increased!</p>}
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  );
}

// useWindowSize hook
function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });
  useEffect(() => {
    const handler = () =>
      setSize({ width: innerWidth, height: innerHeight });
    window.addEventListener("resize", handler);
    return () => removeEventListener("resize", handler);
  }, []);
  return size;
}

useToggle 与 useClipboard

useToggle 通过包裹在 useCallback 中的 toggle 函数简化布尔状态以获得稳定标识。useClipboard 包裹剪贴板 API,带有在超时后自动重置的 'copied' 反馈状态。这些小型实用 hook 减少样板代码并标准化应用中的常见模式。两个 hook 中的 useCallback 防止记忆化子组件的不必要重新渲染。构建小型、专注的 hook 库(useToggle、useClipboard、useMediaQuery、useOnClickOutside)加速开发并确保一致行为。

react
function useToggle(initial = false) {
  const [on, setOn] = useState(initial);
  const toggle = useCallback(() => setOn((p) => !p), []);
  return [on, toggle, setOn];
}

function useClipboard(timeout = 2000) {
  const [copied, setCopied] = useState(false);
  const copy = useCallback((text) => {
    navigator.clipboard.writeText(text).then(() => {
      setCopied(true);
      setTimeout(() => setCopied(false), timeout);
    });
  }, [timeout]);
  return [copied, copy];
}

// Usage
function CopyButton({ text }) {
  const [copied, copy] = useClipboard();
  return (
    <button onClick={() => copy(text)}>
      {copied ? "Copied!" : "Copy"}
    </button>
  );
}

function Modal({ children }) {
  const [isOpen, toggle] = useToggle(false);
  return (
    <>
      <button onClick={toggle}>Open</button>
      {isOpen && <div className="modal">{children}</div>}
    </>
  );
}
12

Portals

创建 Portal

createPortal 将子元素渲染到当前组件层次结构之外的 DOM 节点(通常是 document.body)。这对必须逃离父 CSS 约束(overflow: hidden、z-index 层叠上下文、transform 创建新上下文)的模态框、工具提示和下拉菜单至关重要。尽管在 DOM 其他位置渲染,portal 的 React 事件冒泡仍然像在原始树中一样工作——祖先上的 onClick 处理程序仍会触发。这给你两全其美:视觉上逃离父约束,但逻辑事件流保留。

react
import { createPortal } from "react-dom";

function Modal({ children, onClose }) {
  return createPortal(
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-content" onClick={(e) => e.stopPropagation()}>
        <button onClick={onClose}>✕</button>
        {children}
      </div>
    </div>,
    document.body  // render target outside the DOM hierarchy
  );
}

// Usage: the modal renders at body level,
// escaping any overflow:hidden or z-index stacking contexts
function App() {
  const [show, setShow] = useState(false);
  return (
    <div style={{ overflow: "hidden", position: "relative" }}>
      <button onClick={() => setShow(true)}>Open Modal</button>
      {show && <Modal onClose={() => setShow(false)}>Hello!</Modal>}
    </div>
  );
}

带 Portal 与焦点陷阱的模态框

生产模态框需要的不仅是 portal:焦点管理(将焦点困在内部,关闭时恢复)、Escape 键处理、body 滚动锁定和点击外部关闭。此实现保存先前聚焦的元素,打开时聚焦模态框,关闭时恢复焦点——对屏幕阅读器用户至关重要。Body overflow hidden 防止背景滚动。清理函数恢复一切。对于完整的焦点陷阱(模态框内的 tab 循环),使用 focus-trap-react 等库。关闭时始终返回 null 以从 DOM 移除。

react
function Modal({ isOpen, onClose, children }) {
  const modalRef = useRef(null);

  useEffect(() => {
    if (!isOpen) return;
    const modal = modalRef.current;
    const previouslyFocused = document.activeElement;
    modal.focus();

    const handleKey = (e) => {
      if (e.key === "Escape") onClose();
    };
    document.addEventListener("keydown", handleKey);

    // Prevent body scroll
    document.body.style.overflow = "hidden";

    return () => {
      document.removeEventListener("keydown", handleKey);
      document.body.style.overflow = "";
      previouslyFocused.focus(); // restore focus
    };
  }, [isOpen, onClose]);

  if (!isOpen) return null;

  return createPortal(
    <div className="overlay" onClick={onClose}>
      <div ref={modalRef} tabIndex={-1} className="modal">
        {children}
      </div>
    </div>,
    document.body
  );
}

带 Portal 的工具提示

工具提示受益于 portal,因为它们必须溢出父容器并避免裁剪。工具提示位置从触发器的 getBoundingClientRect() 计算,并使用 position: fixed 在 body 级别渲染。这避免了 z-index 和 overflow 问题。对于动态定位(靠近屏幕边缘时翻转),使用 Floating UI(前身为 Popper.js)等库。portal 确保工具提示永远不会被 overflow: hidden 祖先裁剪。固定定位坐标相对于视口,使计算简单。

react
function Tooltip({ children, text }) {
  const [visible, setVisible] = useState(false);
  const [coords, setCoords] = useState({ x: 0, y: 0 });
  const targetRef = useRef(null);

  const show = () => {
    const rect = targetRef.current.getBoundingClientRect();
    setCoords({ x: rect.left, y: rect.top - 40 });
    setVisible(true);
  };

  return (
    <>
      <span
        ref={targetRef}
        onMouseEnter={show}
        onMouseLeave={() => setVisible(false)}
      >
        {children}
      </span>
      {visible && createPortal(
        <div style={{ position: "fixed", left: coords.x, top: coords.y }}
             className="tooltip">
          {text}
        </div>,
        document.body
      )}
    </>
  );
}

// Usage
<Tooltip text="Click to save">💾</Tooltip>

带 Portal 的下拉菜单

下拉菜单面临与工具提示相同的 overflow/z-index 问题。portal 解决视觉问题。点击外部处理程序检查点击目标是否在触发器 ref 之外。捕获阶段滚动监听器(第三个参数为 true)在任何滚动时关闭菜单,防止菜单与触发器分离。对于生产,使用 Floating UI,它处理边缘检测、翻转、移动和滚动/调整大小时的自动定位更新。Portal + 正确定位逻辑 = 在任何布局上下文中工作的稳健下拉菜单。

react
function Dropdown({ trigger, children }) {
  const [open, setOpen] = useState(false);
  const [pos, setPos] = useState({ top: 0, left: 0 });
  const ref = useRef(null);

  const handleOpen = () => {
    const rect = ref.current.getBoundingClientRect();
    setPos({ top: rect.bottom + 4, left: rect.left });
    setOpen(true);
  };

  useEffect(() => {
    if (!open) return;
    const handleClick = (e) => {
      if (!ref.current?.contains(e.target)) setOpen(false);
    };
    const handleScroll = () => setOpen(false); // close on scroll
    document.addEventListener("mousedown", handleClick);
    window.addEventListener("scroll", handleScroll, true);
    return () => {
      document.removeEventListener("mousedown", handleClick);
      window.removeEventListener("scroll", handleScroll, true);
    };
  }, [open]);

  return (
    <>
      <div ref={ref} onClick={handleOpen}>{trigger}</div>
      {open && createPortal(
        <div style={{ position: "fixed", ...pos }} className="dropdown">
          {children}
        </div>,
        document.body
      )}
    </>
  );
}

Portal 事件冒泡

React Portal 的关键特性:事件冒泡遵循 React 组件树而非 DOM 树。父组件上的 onClick 即使在子组件被 portal 到 document.body 时也会触发。这意味着 context、状态和事件委托都自然工作。然而,CSS 继承不会跨越 portal 边界——父组件上的样式不会层叠到 portal 内容,因为它们在不同的 DOM 子树中。必须显式应用 CSS(通过类或 :root 上的 CSS 变量)来设置 portal 内容样式。这种分离对于模态框/工具提示通常是可取的。

react
function PortalExample() {
  // Despite rendering in document.body, events bubble
  // through the React tree, not the DOM tree
  return (
    <div onClick={() => console.log("Parent clicked!")}>
      <p>Click the button — parent handler fires!</p>
      {createPortal(
        <button onClick={() => console.log("Button clicked!")}>
          I'm in a portal
        </button>,
        document.body
      )}
    </div>
  );
}
// Clicking logs: "Button clicked!" then "Parent clicked!"

// This means context, state, and event delegation
// all work as if the portal were inline

// But CSS inheritance does NOT cross the portal boundary:
// document.body styles won't inherit into the portal content
// unless you explicitly apply them
13

Suspense 与惰性加载

React.lazy 与 Suspense

React.lazy 动态导入组件,创建按需加载的单独包(代码分割)。将惰性组件包裹在 <Suspense> 中,带有在 chunk 下载时显示的 fallback(加载状态)。这减少了初始包大小——用户仅下载他们访问页面的代码。每个 lazy() 调用创建单独的 chunk。对于基于路由的分割,惰性加载每个页面组件。fallback 可以是任何 React 节点(加载动画、骨架屏、文本)。Suspense 可以包裹多个惰性组件——fallback 显示直到全部就绪。

react
import { lazy, Suspense } from "react";

// Lazy-load component (code-split)
const Dashboard = lazy(() => import("./Dashboard"));
const Settings = lazy(() => import("./Settings"));

function App() {
  return (
    <Suspense fallback={<div>Loading page...</div>}>
      <nav>
        <button onClick={() => setPage("dash")}>Dashboard</button>
        <button onClick={() => setPage("settings")}>Settings</button>
      </nav>
      {page === "dash" && <Dashboard />}
      {page === "settings" && <Settings />}
    </Suspense>
  );
}

嵌套 Suspense

嵌套 Suspense 边界创建'剥落'效果,内容随着每个 chunk 加载而逐步显示。外部 Suspense 首先显示其 fallback;随着内部组件加载,它们独立显示。这防止单个慢组件阻塞整个页面。策略性地放置 Suspense 边界:路由级页面周围(粗)、主要部分周围(中)、独立小部件周围(细)。太多边界创建卡顿加载;太少创建长等待。关键是匹配边界到用户感知的内容单元。

react
<Suspense fallback={<PageSkeleton />}>
  <Header />
  <Suspense fallback={<MainSkeleton />}>
    <MainContent />  {/* loads first */}
    <Suspense fallback={<CommentsSkeleton />}>
      <Comments />  {/* loads independently, doesn't block MainContent */}
    </Suspense>
  </Suspense>
  <Sidebar />
</Suspense>

// Suspense "peeling" effect:
// 1. PageSkeleton shows
// 2. Header + Sidebar load → PageSkeleton peels away
// 3. MainSkeleton shows until MainContent loads
// 4. MainContent shows, CommentsSkeleton shows
// 5. Comments load → everything visible

惰性加载与错误边界

惰性加载可能失败(网络问题、部署使 chunk URL 失效)。错误边界捕获这些错误并显示回退 UI。始终将 Suspense + lazy 包裹在 ErrorBoundary 中。componentDidCatch 记录错误以供监控。对于重试逻辑,可以重置 ErrorBoundary 状态或重新加载页面。常见模式是重新导入 chunk 的重试按钮。没有错误边界,失败的 chunk 加载会使整个应用崩溃。这对生产至关重要——网络可靠性永远不是 100%。

react
class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null };
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }
  componentDidCatch(error, info) {
    console.error("Chunk load failed:", error, info);
  }
  render() {
    if (this.state.hasError) {
      return (
        <div>
          <p>Failed to load. {this.state.error.message}</p>
          <button onClick={() => window.location.reload()}>
            Retry
          </button>
        </div>
      );
    }
    return this.props.children;
  }
}

// Wrap lazy components: network failures need handling
<ErrorBoundary>
  <Suspense fallback={<Loader />}>
    <LazyComponent />
  </Suspense>
</ErrorBoundary>

使用 Suspense 获取数据

React 19 的 use() hook 启用 Suspense 用于数据获取。与 useEffect 不同,use() 挂起组件直到 promise 解析——最近的 Suspense 边界显示其 fallback。同一组件中的多个 use() 调用并发解析(并行获取)。这消除了手动加载状态管理。promise 可以在 React 外部缓存以防止重新渲染时重新获取。注意:use() 只能在渲染或 hook 内部调用。对于 React 18,使用 React Query 或 SWR 等与 Suspense 集成的库。

react
// React 18+ Suspense for data fetching (experimental)
import { use } from "react"; // React 19+

// Wrap a promise with use()
function UserProfile({ userId }) {
  // 'use' suspends until the promise resolves
  const user = use(fetchUser(userId));
  return <div>{user.name}</div>;
}

function fetchUser(id) {
  return fetch(`/api/users/${id}`).then((r) => r.json());
}

// Parent provides Suspense boundary
function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <UserProfile userId={1} />
    </Suspense>
  );
}

// Concurrent: multiple suspends resolve together
function Dashboard() {
  const user = use(fetchUser(1));
  const posts = use(fetchPosts(user.id));
  // Both fetch in parallel, Suspense shows until all resolve
  return <div>{user.name}: {posts.length} posts</div>;
}

Suspense List(编排)

SuspenseList 编排多个 Suspense 边界的显示顺序。revealOrder='forwards' 按顺序显示项(项 2 在项 1 就绪前不会显示,即使 2 先加载)——防止内容跳动。'together' 等待全部就绪后显示。'backwards' 从底到顶显示。tail='collapsed' 隐藏尚未开始的项的加载状态;'hidden' 隐藏所有 fallback。这对顺序重要的信息流和列表有用。注意:SuspenseList 是实验性的,其 API 可能变化——查看当前 React 文档了解可用性。

react
// SuspenseList controls reveal order of multiple Suspense
import { SuspenseList, Suspense } from "react";

function Article({ id }) {
  const data = use(fetchArticle(id));
  return <article>{data.title}</article>;
}

function Feed() {
  return (
    <SuspenseList revealOrder="forwards" tail="collapsed">
      {/* "forwards": reveals top-to-bottom as they load */}
      {/* "together": waits for all, reveals together */}
      {/* "backwards": reveals bottom-to-top */}

      <Suspense fallback={<Skeleton />}>
        <Article id={1} />
      </Suspense>
      <Suspense fallback={<Skeleton />}>
        <Article id={2} />
      </Suspense>
      <Suspense fallback={<Skeleton />}>
        <Article id={3} />
      </Suspense>
    </SuspenseList>
  );
}
14

React Router

基础路由

React Router v6 使用 <BrowserRouter> 作为根,<Routes> 定义路由匹配,<Route> 带 element prop(而非 component)。<Link> 创建使用 History API 的导航链接(无页面重载)。动态段(:id)通过 useParams() 访问。path='*' 是 404 的全匹配。路由按最佳匹配而非顺序匹配。对于 URL 搜索参数(?q=search),使用 useSearchParams()。BrowserRouter 需要服务器配置以对所有路由提供 index.html(SPA 回退)。

react
import { BrowserRouter, Routes, Route, Link } from "react-router-dom";

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
        <Link to="/users/123">User 123</Link>
      </nav>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/users/:id" element={<UserPage />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </BrowserRouter>
  );
}

function UserPage() {
  const { id } = useParams();
  return <h1>User ID: {id}</h1>;
}

嵌套路由与 Outlet

嵌套路由创建布局层次结构。父路由的 element 必须包含 <Outlet />,子路由在此渲染。index 路由在父路径渲染。深度嵌套路由(users/:id)创建嵌套布局——Users 布局包裹 UserDetail。这对带持久侧边栏/页眉的仪表板很有用。useOutlet() 访问子元素。URL /users/123 渲染 Layout → Users → UserDetail,每个贡献其布局。这替代了手动条件渲染布局。

react
function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Layout />}>
          <Route index element={<Home />} />
          <Route path="about" element={<About />} />
          <Route path="users" element={<Users />}>
            <Route path=":id" element={<UserDetail />} />
          </Route>
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

function Layout() {
  return (
    <div>
      <nav>Navigation here</nav>
      <Outlet />  {/* Child routes render here */}
    </div>
  );
}

function Users() {
  return (
    <div>
      <h2>Users</h2>
      <Outlet />  {/* Nested :id route renders here */}
    </div>
  );
}

导航与重定向

useNavigate 返回用于编程式导航的函数。navigate('/path', { replace: true }) 替换历史(无后退按钮)。传递 state 以将数据带到下一个路由(例如登录后返回哪里)。<Navigate> 是声明式重定向组件——在渲染中用于认证守卫。NavLink 提供 isActive 用于设置活动链接样式。useLocation 提供当前 URL、pathname、search、hash 和 state。对于操作后重定向(表单提交),使用 navigate。对于渲染中的条件重定向,使用 <Navigate>。

react
import { useNavigate, Navigate, NavLink, useLocation } from "react-router-dom";

function Login() {
  const navigate = useNavigate();
  const location = useLocation();

  const handleLogin = async () => {
    await auth.login();
    // Redirect to intended page or home
    const from = location.state?.from || "/";
    navigate(from, { replace: true });
  };
  return <button onClick={handleLogin}>Login</button>;
}

// Declarative redirect
function ProtectedRoute({ user, children }) {
  if (!user) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }
  return children;
}

// NavLink: active styling
<NavLink to="/about" className={({ isActive }) =>
  isActive ? "nav-active" : "nav"
}>
  About
</NavLink>

加载器与数据加载

React Router v6.4+(数据路由)添加了加载器(在路由渲染前运行)和 action(处理表单提交)。useLoaderData() 访问加载器数据——不再需要 useEffect 获取路由数据。加载器为嵌套路由并行运行。errorElement 捕获来自加载器/action 的错误。Action 通过 <Form method='post'> 处理表单提交——useActionData() 返回结果。此模式(受 Remix 启发)将数据逻辑与路由共置。数据 API 需要 createBrowserRouter/createHashRouter,而非 <BrowserRouter>。

react
import { createBrowserRouter, RouterProvider } from "react-router-dom";

const router = createBrowserRouter([
  {
    path: "/users/:id",
    element: <UserPage />,
    loader: async ({ params }) => {
      const res = await fetch(`/api/users/${params.id}`);
      if (!res.ok) throw new Response("Not found", { status: 404 });
      return res.json();
    },
    errorElement: <ErrorPage />,
  },
]);

function UserPage() {
  const user = useLoaderData(); // data from loader
  return <h1>{user.name}</h1>;
}

// Action for form submissions
{
  path: "/users/new",
  element: <NewUser />,
  action: async ({ request }) => {
    const formData = await request.formData();
    const res = await fetch("/api/users", {
      method: "POST",
      body: formData,
    });
    return redirect(`/users/${res.id}`);
  },
}

function App() {
  return <RouterProvider router={router} />;
}

路由守卫与受保护路由

路由守卫根据认证状态或角色保护路由。RequireAuth 将未认证用户重定向到登录,将预期目标保存在 location.state 中用于登录后重定向。RequireRole 添加基于角色的访问控制。通过包裹组合守卫(RequireAuth > RequireRole > component)。对于布局路由,也可以使用 element={<RequireAuth><Outlet/></RequireAuth>} 一次保护所有子路由。始终也在服务器上检查认证——客户端守卫是为了用户体验而非安全。此模式可扩展到任何条件:订阅、功能标志等。

react
function RequireAuth({ children }) {
  const { user } = useAuth();
  const location = useLocation();

  if (!user) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }
  return children;
}

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/login" element={<Login />} />
        <Route path="/" element={<Layout />}>
          <Route index element={<Home />} />
          <Route path="dashboard" element={
            <RequireAuth><Dashboard /></RequireAuth>
          } />
          <Route path="admin" element={
            <RequireAuth><RequireRole role="admin"><Admin /></RequireRole></RequireAuth>
          } />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

function RequireRole({ role, children }) {
  const { user } = useAuth();
  if (user?.role !== role) return <Navigate to="/forbidden" />;
  return children;
}
15

状态管理(Context 与 Redux)

Context API 模式

Context 提供无需 prop 逐层传递的全局状态。创建 context,用 Provider 包裹消费者,通过 useContext 访问。自定义 useAuth hook 添加错误检查,是推荐的 API 表面。Context 非常适合低频更新(认证、主题、语言环境)。对于高频状态变化,Context 导致所有消费者在每次变化时重新渲染——对复杂状态使用 useReducer 或拆分 context。始终将 Provider 与其管理的状态共置。Context 值如果包含函数应使用 useMemo/useCallback 记忆化。

react
const AuthContext = createContext(null);

function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const login = async (credentials) => {
    const user = await api.login(credentials);
    setUser(user);
  };
  const logout = () => setUser(null);
  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

function useAuth() {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error("useAuth must be inside AuthProvider");
  return ctx;
}

// Usage
function Navbar() {
  const { user, logout } = useAuth();
  return user
    ? <button onClick={logout}>Logout {user.name}</button>
    : <Link to="/login">Login</Link>;
}

// Wrap app: <AuthProvider><App /></AuthProvider>

useReducer + Context

useReducer + Context 是无外部库的复杂全局状态的推荐模式。reducer 集中状态逻辑(可预测、可测试的转换)。Provider 记忆化值以防止不必要的重新渲染。派生值(total)在 useMemo 中计算。此模式处理购物车、表单状态、多步向导等。对于需要中间件、时间旅行调试或许多独立切片的真正复杂应用,考虑 Redux Toolkit 或 Zustand。但对于大多数应用,useReducer + Context 足够且零依赖。

react
const CartContext = createContext();

function cartReducer(state, action) {
  switch (action.type) {
    case "ADD":
      const existing = state.find((i) => i.id === action.item.id);
      if (existing) {
        return state.map((i) =>
          i.id === action.item.id ? { ...i, qty: i.qty + 1 } : i
        );
      }
      return [...state, { ...action.item, qty: 1 }];
    case "REMOVE":
      return state.filter((i) => i.id !== action.id);
    case "CLEAR":
      return [];
    default:
      return state;
  }
}

function CartProvider({ children }) {
  const [items, dispatch] = useReducer(cartReducer, []);
  const value = useMemo(() => ({
    items,
    total: items.reduce((s, i) => s + i.price * i.qty, 0),
    addItem: (item) => dispatch({ type: "ADD", item }),
    removeItem: (id) => dispatch({ type: "REMOVE", id }),
  }), [items]);
  return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
}

Redux Toolkit 基础

Redux Toolkit (RTK) 是使用 Redux 的现代推荐方式。createSlice 自动生成 action 创建器和 reducer。它内部使用 Immer,因此你'直接修改'状态(state.value += 1),Immer 产生不可变更新。configureStore 以合理默认值设置 store(Redux DevTools、thunk 中间件)。useSelector 读取状态;useDispatch 分发 action。RTK 消除 Redux 样板代码(无 switch 语句、无 action 类型常量)。对于异步逻辑,使用 createAsyncThunk。RTK Query(包含)处理数据获取和缓存。

react
import { configureStore, createSlice } from "@reduxjs/toolkit";
import { useSelector, useDispatch } from "react-redux";

const counterSlice = createSlice({
  name: "counter",
  initialState: { value: 0 },
  reducers: {
    increment: (state) => { state.value += 1; }, // Immer: mutate safely
    decrement: (state) => { state.value -= 1; },
    addBy: (state, action) => { state.value += action.payload; },
  },
});

const store = configureStore({
  reducer: { counter: counterSlice.reducer },
});

export const { increment, decrement, addBy } = counterSlice.actions;

// Usage in component
function Counter() {
  const count = useSelector((state) => state.counter.value);
  const dispatch = useDispatch();
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => dispatch(increment())}>+</button>
      <button onClick={() => dispatch(addBy(5))}>+5</button>
    </div>
  );
}

Zustand(轻量级替代方案)

Zustand 是一个最小状态管理库——无 Provider、无样板代码。使用 create() 创建 store,通过带选择器函数的 hook 访问。选择器防止重新渲染:仅使用已变化切片的组件重新渲染。这解决了 Context 的重新渲染问题而无 Redux 的复杂性。对于对象选择器(返回 {a, b}),使用浅比较防止不必要重新渲染。Zustand 支持中间件(persist、devtools、immer)。它非常适合 Redux 过度但 Context 导致过多重新渲染的小到中型应用。API 小但强大。

react
import { create } from "zustand";

const useStore = create((set, get) => ({
  count: 0,
  user: null,
  increment: () => set((state) => ({ count: state.count + 1 })),
  setUser: (user) => set({ user }),
  reset: () => set({ count: 0, user: null }),
  // Access other state with get()
  doubleCount: () => get().count * 2,
}));

// Usage: select only what you need (prevents re-renders)
function Counter() {
  const count = useStore((state) => state.count);
  const increment = useStore((state) => state.increment);
  return <button onClick={increment}>{count}</button>;
}

// Multiple selections
function Profile() {
  const { user, setUser } = useStore(
    (state) => ({ user: state.user, setUser: state.setUser })
  );
  // Use shallow comparison for object selectors
  // import { shallow } from "zustand/shallow";
  // useStore(selector, shallow);
}

React Query(服务器状态)

React Query (TanStack Query) 管理服务器状态——从 API 获取的数据。它自动处理缓存、后台重新获取、过期数据、乐观更新和分页。queryKey 标识缓存数据(类似缓存键)。staleTime 控制数据被视为新鲜的时间。变更后 invalidateQueries 重新获取依赖查询。与 Redux(客户端状态)不同,React Query 专为异步服务器数据构建。它消除了手动加载/错误状态、useEffect 获取和缓存管理。对于大多数应用,React Query + 本地状态(useState/useReducer)完全替代 Redux。

react
import { useQuery, useMutation, QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Users />
    </QueryClientProvider>
  );
}

function Users() {
  const { data, isLoading, error, refetch } = useQuery({
    queryKey: ["users"],
    queryFn: () => fetch("/api/users").then((r) => r.json()),
    staleTime: 60000, // fresh for 60s
    refetchOnWindowFocus: true,
  });

  const mutation = useMutation({
    mutationFn: (newUser) =>
      fetch("/api/users", { method: "POST", body: JSON.stringify(newUser) }),
    onSuccess: () => queryClient.invalidateQueries(["users"]),
  });

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;
  return (
    <div>
      {data.map((u) => <div key={u.id}>{u.name}</div>)}
      <button onClick={() => mutation.mutate({ name: "New" })}>Add</button>
    </div>
  );
}
16

测试(React Testing Library)

基础组件测试

React Testing Library (RTL) 按用户交互方式测试组件——通过角色、标签和文本,而非实现细节。getByRole 是首选查询(也测试可访问性)。userEvent 比 fireEvent 更准确地模拟真实用户交互(输入、点击)。测试应避免测试内部状态;相反,验证可见输出和行为。如果无法按角色查询,使用 getByLabelText、getByText 或 getByDisplayValue。除非必要避免 getByTestId。此方法使测试对重构具有弹性——它们测试用户看到和做的事情。

react
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Counter } from "./Counter";

test("counter increments on click", async () => {
  const user = userEvent.setup();
  render(<Counter />);

  // Find by accessible role (not test-id)
  expect(screen.getByRole("heading")).toHaveTextContent("0");

  await user.click(screen.getByRole("button", { name: /increment/i }));

  expect(screen.getByRole("heading")).toHaveTextContent("1");
});

test("displays error for invalid input", async () => {
  const user = userEvent.setup();
  render(<Form />);

  await user.type(screen.getByLabelText(/email/i), "not-an-email");
  await user.click(screen.getByRole("button", { name: /submit/i }));

  expect(screen.getByRole("alert")).toHaveTextContent(/invalid email/i);
});

测试 Hooks

renderHook 隔离测试自定义 hook。result.current 持有 hook 的返回值。所有状态更新必须包裹在 act() 中以确保 React 同步处理它们。rerender 让你测试依赖变化 props 的 effect。对于异步 hook(带 fetch 的 useEffect),使用 waitFor 或 findBy 查询(等待更新)。直接测试 hook 比通过组件测试更快更专注。然而,也通过组件集成测试验证真实世界使用。renderHook 在 @testing-library/react v13+ 中可用。

react
import { renderHook, act } from "@testing-library/react";
import { useCounter } from "./useCounter";

test("useCounter increments and decrements", () => {
  const { result } = renderHook(() => useCounter(0));

  expect(result.current.count).toBe(0);

  // Wrap state updates in act()
  act(() => result.current.increment());
  expect(result.current.count).toBe(1);

  act(() => result.current.decrement());
  expect(result.current.count).toBe(0);

  act(() => result.current.reset());
  expect(result.current.count).toBe(0);
});

// Testing with initial props that change
test("useEffect runs on dependency change", () => {
  const { result, rerender } = renderHook(
    ({ id }) => useFetchUser(id),
    { initialProps: { id: 1 } }
  );
  rerender({ id: 2 });
  // Effect re-ran with new id
});

测试异步与模拟

MSW (Mock Service Worker) 在 service worker 级别拦截网络请求——测试使用真实 fetch() 但获得模拟响应。这比直接模拟 fetch 更真实。setupServer 用于 Node (Jest),setupWorker 用于浏览器。beforeAll/afterAll 生命周期管理服务器。server.use() 按测试覆盖处理程序。findBy 查询(异步)等待元素出现——用于异步渲染。queryBy 如果未找到返回 null(用于断言不存在)。waitFor 轮询条件。MSW 也可用于开发模拟和 Storybook。

react
import { render, screen, waitFor } from "@testing-library/react";
import { rest } from "msw";
import { setupServer } from "msw/node";
import { UserProfile } from "./UserProfile";

// Mock API with MSW (Mock Service Worker)
const server = setupServer(
  rest.get("/api/users/:id", (req, res, ctx) => {
    return res(ctx.json({ id: 1, name: "Alice" }));
  })
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

test("displays user after fetch", async () => {
  render(<UserProfile id={1} />);

  // findBy waits for async update
  expect(await screen.findByText("Alice")).toBeInTheDocument();
  expect(screen.queryByText("Loading")).not.toBeInTheDocument();
});

test("shows error on fetch failure", async () => {
  server.use(
    rest.get("/api/users/:id", (req, res, ctx) =>
      res(ctx.status(500))
    )
  );
  render(<UserProfile id={1} />);
  expect(await screen.findByText(/error/i)).toBeInTheDocument();
});

测试 Context 与 Provider

创建自定义 render 工具,用所需 Provider(Theme、Auth、Router 等)包裹组件。这避免在每个测试中重复 Provider 设置。从 test-utils 文件重新导出 RTL 函数,使测试从那里导入。对于路由测试,使用 MemoryRouter(而非 BrowserRouter)配 initialEntries 设置起始 URL——无需真实浏览器历史。对于 Redux,用真实或模拟 store 包裹在测试 Provider 中。此模式保持测试干净并确保所有组件有所需 context。这是任何 React 测试基础设施的标准设置。

react
// Custom render that wraps with providers
import { render } from "@testing-library/react";
import { ThemeProvider } from "./ThemeProvider";

function customRender(ui, { theme = "light", ...options } = {}) {
  function Wrapper({ children }) {
    return <ThemeProvider initialTheme={theme}>{children}</ThemeProvider>;
  }
  return render(ui, { wrapper: Wrapper, ...options });
}

// Re-export everything
export * from "@testing-library/react";
export { customRender as render };

// In test files, import from your test-utils:
// import { render, screen } from "../test-utils";

test("button uses theme color", () => {
  customRender(<Button>Click</Button>, { theme: "dark" });
  expect(screen.getByRole("button")).toHaveClass("btn-dark");
});

// Testing with router
import { MemoryRouter } from "react-router-dom";
render(
  <MemoryRouter initialEntries={["/users/123"]}>
    <App />
  </MemoryRouter>
);

测试事件与交互

userEvent.setup() 创建用于真实交互的用户实例:type(逐字符)、click、tab、keyboard(带 {Escape}、{Enter} 等键码)、selectOptions、upload 等。始终 await 用户交互——它们是异步的。测试键盘导航对可访问性至关重要。对于表单测试,填写所有字段并验证 onSubmit 接收正确数据。userEvent 优于 fireEvent,因为它模拟真实浏览器行为(focus、blur、input 事件按正确顺序)。测试完整用户流程,而非单个事件处理程序。

react
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

test("form submission flow", async () => {
  const onSubmit = jest.fn();
  const user = userEvent.setup();
  render(<LoginForm onSubmit={onSubmit} />);

  // Fill form fields
  await user.type(screen.getByLabelText(/email/i), "[email protected]");
  await user.type(screen.getByLabelText(/password/i), "password123");

  // Submit
  await user.click(screen.getByRole("button", { name: /login/i }));

  expect(onSubmit).toHaveBeenCalledWith({
    email: "[email protected]",
    password: "password123",
  });
});

test("keyboard navigation", async () => {
  const user = userEvent.setup();
  render(<Modal />);
  await user.tab(); // focus first element
  expect(screen.getByRole("button", { name: /close/i })).toHaveFocus();
  await user.keyboard("{Escape}"); // press Escape
  expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
17

TypeScript + React

组件 Props 类型

TypeScript 与 React 为 props 提供类型安全。为 props 使用接口或类型别名。可选 props 使用 ?。联合类型(variant)约束值。React.ReactNode 接受任何可渲染内容(字符串、元素、数组)。扩展 HTML 属性(React.InputHTMLAttributes)让你的组件接受所有原生属性(placeholder、onChange 等)同时添加自定义 props。展开 {...rest} 将剩余属性传递给原生元素。此模式创建类型安全、灵活的组件。始终导出 prop 类型以便消费者引用。

react
// Basic props
interface ButtonProps {
  text: string;
  onClick: () => void;
  variant?: "primary" | "secondary"; // union type
  disabled?: boolean;
}

function Button({ text, onClick, variant = "primary", disabled }: ButtonProps) {
  return (
    <button
      className={`btn btn-${variant}`}
      onClick={onClick}
      disabled={disabled}
    >
      {text}
    </button>
  );
}

// Children prop
interface CardProps {
  title: string;
  children: React.ReactNode; // any renderable content
}

// Extending HTML attributes
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  label: string;
  error?: string;
}

function Input({ label, error, ...rest }: InputProps) {
  return (
    <label>
      {label}
      <input {...rest} />
      {error && <span className="error">{error}</span>}
    </label>
  );
}

带 TypeScript 的 Hooks

TypeScript 为 hook 添加类型安全。useState<T> 指定状态类型;useState<T | null>(null) 用于可空状态。useRef<T>(null) 类型化 ref——current 是 T | null。对于 useContext,定义 context 类型并在 undefined 时抛出(使消费者获得非 undefined 类型)。对于 useReducer,将 Action 类型化为可辨识联合——对 action.type 的 switch 在每个 case 中缩小类型,提供类型安全的 payload 访问。这些模式消除了 undefined 访问和不正确 action payload 的运行时错误。

react
// useState with types
const [count, setCount] = useState<number>(0);
const [user, setUser] = useState<User | null>(null);
const [items, setItems] = useState<string[]>([]);

// useRef
const inputRef = useRef<HTMLInputElement>(null);
// Access: inputRef.current?.focus()

// useContext
interface ThemeContextType {
  theme: "light" | "dark";
  toggle: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

function useTheme() {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error("useTheme must be inside ThemeProvider");
  return ctx; // ctx is now ThemeContextType, not undefined
}

// useReducer
type Action = { type: "increment" } | { type: "set"; value: number };
const [state, dispatch] = useReducer((state: number, action: Action) => {
  switch (action.type) {
    case "increment": return state + 1;
    case "set": return action.value;
  }
}, 0);

泛型组件

泛型组件和 hook 处理任何数据类型同时保持类型安全。<T> 类型参数从 items prop 推断,因此 renderItem 和 keyExtractor 自动接收正确类型。这就是 TypeScript 如何重新创建具有完整类型安全的通用实用组件(List、Table、Select)。泛型 hook(useArray<T>)类似地通过操作保留类型。关键洞察:TypeScript 从使用推断 T,因此消费者很少需要显式指定。此模式对于构建可重用、类型安全的组件库至关重要。

react
// Generic component: works with any type
interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  keyExtractor: (item: T) => string;
}

function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map((item) => (
        <li key={keyExtractor(item)}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

// Usage: TypeScript infers T from items
<List
  items={[{ id: "1", name: "Alice" }, { id: "2", name: "Bob" }]}
  renderItem={(user) => <span>{user.name}</span>}
  keyExtractor={(user) => user.id}
/>

// Generic hook
function useArray<T>(initial: T[]) {
  const [array, setArray] = useState(initial);
  const push = (item: T) => setArray((prev) => [...prev, item]);
  return { array, push, setArray };
}

事件类型与 Refs

React 事件类型是特定的:输入用 ChangeEvent、表单用 FormEvent、点击用 MouseEvent。每个都是元素类型的泛型(e.target 正确类型化)。带 TypeScript 的 forwardRef 需要两个类型参数:ref 类型和 props 类型。当组件需要向 DOM 元素暴露 ref(用于焦点、测量等)时需要 forwardRef。始终为 forwardRef/memo 组件设置 displayName 以获得更好的 DevTools 调试。React 19 允许 ref 作为常规 prop,减少 forwardRef 的需求,但在现有代码中仍然常见。

react
// Event types
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
  console.log(e.target.value); // string
}

function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
  e.preventDefault();
  const formData = new FormData(e.currentTarget);
}

function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
  console.log(e.clientX, e.clientY);
}

// forwardRef with TypeScript
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  label: string;
}

const Input = React.forwardRef<HTMLInputElement, InputProps>(
  ({ label, ...props }, ref) => (
    <label>
      {label}
      <input ref={ref} {...props} />
    </label>
  )
);
Input.displayName = "Input";

// Usage: const ref = useRef<HTMLInputElement>(null);
// <Input ref={ref} label="Email" />

Props 的实用类型

TypeScript 实用类型对 prop 组合很强大。Pick 选择特定 props(用于子集组件)。Omit 排除 props(用于替换行为)。Partial 使所有 props 可选(用于默认 prop 模式)。ComponentProps<typeof Component> 提取组件的 prop 类型——用于包裹/扩展现有组件。Record<K, V> 创建将键映射到值的类型(非常适合 variant 到类的映射)。这些实用类型实现 DRY、类型安全的 prop 定义而无需重复接口。掌握它们以编写可维护的 React + TypeScript 代码。

react
// Pick: select specific props
interface ButtonProps {
  text: string;
  onClick: () => void;
  color: string;
  size: "sm" | "md" | "lg";
}
type IconButtonProps = Pick<ButtonProps, "onClick" | "size"> & {
  icon: React.ReactNode;
};

// Omit: exclude specific props
type LinkButtonProps = Omit<ButtonProps, "onClick"> & {
  href: string;
};

// Partial: all props optional (for defaults)
type DefaultProps = Partial<ButtonProps>;

// ComponentProps: extract props from existing component
type MyButtonProps = React.ComponentProps<typeof Button> & {
  variant?: "custom";
};

// ReturnType: type of a function's return
type User = ReturnType<typeof fetchUser>;

// Record for prop maps
type ButtonVariants = Record<"primary" | "danger" | "ghost", string>;
18

并发特性(useTransition、useDeferredValue)

useTransition

useTransition 将状态更新标记为非紧急(过渡)。紧急更新(输入值)立即渲染以保持响应性;非紧急更新(过滤 10,000 项)可在用户再次输入时被中断。isPending 表示过渡正在进行(显示细微加载指示器)。这防止 UI 在昂贵渲染期间冻结。关键洞察:React 可以中断并丢弃过期过渡,保持 UI 响应。用于搜索过滤、标签切换和任何触发重型渲染的状态更新。不要将紧急更新(输入、点击)包裹在过渡中。

react
import { useTransition, useState } from "react";

function SearchResults() {
  const [isPending, startTransition] = useTransition();
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([]);

  const handleSearch = (value) => {
    setQuery(value); // urgent: update input immediately
    startTransition(() => {
      // non-urgent: heavy filtering can be interrupted
      const filtered = heavyFilter(allItems, value);
      setResults(filtered);
    });
  };

  return (
    <div>
      <input value={query} onChange={(e) => handleSearch(e.target.value)} />
      {isPending && <span>Updating...</span>}
      <ul>{results.map((r) => <li key={r.id}>{r.name}</li>)}</ul>
    </div>
  );
}

useDeferredValue

useDeferredValue 是 useTransition 的声明式对应物。它返回以较低优先级更新的值的延迟副本。输入立即更新(紧急);昂贵列表以延迟值重新渲染(非紧急)。Results 组件上的 React.memo 至关重要——它防止每次按键都重新渲染,仅在 deferredQuery 变化时重新渲染。isStale(比较当前与延迟)让你显示视觉指示器(变暗、加载动画)。当无法控制状态更新(例如值来自 props)时使用 useDeferredValue。当控制更新时使用 useTransition。

react
function Search() {
  const [query, setQuery] = useState("");
  // query updates immediately; deferredQuery lags behind
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;

  return (
    <div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <Results query={deferredQuery} isStale={isStale} />
    </div>
  );
}

// Memoize the expensive component so it only re-renders
// when deferredQuery changes (not on every keystroke)
const Results = React.memo(function Results({ query, isStale }) {
  const items = expensiveSearch(query); // heavy computation
  return (
    <div style={{ opacity: isStale ? 0.5 : 1 }}>
      {items.map((i) => <div key={i.id}>{i.name}</div>)}
    </div>
  );
});

useOptimistic(React 19)

useOptimistic (React 19) 实现乐观更新——UI 立即以预期结果更新,然后与实际服务器响应协调。乐观状态在异步操作期间显示;当真实数据到达(组件以新 props 重新渲染)时,乐观值自动替换。如果操作失败,乐观更新在下次以未更改 props 渲染时简单回退。这消除了手动乐观更新逻辑(跟踪待处理状态、错误时回退)。标记待处理项(pending: true)以显示加载指示器。非常适合点赞、评论和切换。

react
import { useOptimistic } from "react";

function ThumbsUp({ likes, addLike }) {
  // Optimistic state: updates immediately, reverts on error
  const [optimisticLikes, addOptimisticLike] = useOptimistic(
    likes,
    (state, newLike) => [...state, newLike]
  );

  const handleClick = async () => {
    const newLike = { id: Date.now(), pending: true };
    addOptimisticLike(newLike); // instant UI update
    try {
      await addLike(newLike); // actual API call
    } catch {
      // Reverts automatically on re-render with real data
    }
  };

  return (
    <div>
      <button onClick={handleClick}>👍 {optimisticLikes.length}</button>
      {optimisticLikes.some((l) => l.pending) && <span>Saving...</span>}
    </div>
  );
}

use(React 19 Hook)

use() 是 React 19 的新 hook,读取 context 或 promise。与 useContext 不同,use() 可以条件调用(在 if 语句、循环内)——它没有 rules-of-hooks 限制。对于 promise,use() 挂起组件直到 promise 解析(需要 Suspense 边界)。promise 在父组件中创建并作为 prop 传递——这在渲染期间开始获取(而非在 useEffect 中),使瀑布流更早开始。同一 promise 可以传递给多个组件(去重)。use() 弥合同步 context 和异步数据之间的差距。

react
import { use } from "react";

// Read context with use (works in conditions!)
function Theme() {
  // Unlike useContext, use() can be inside conditions
  if (showTheme) {
    const theme = use(ThemeContext); // conditional context!
    return <div style={{ background: theme.color }} />;
  }
  return null;
}

// Read promises with use (Suspense integration)
function UserProfile({ userPromise }) {
  // Suspends until promise resolves
  const user = use(userPromise);
  return <h1>{user.name}</h1>;
}

// Parent passes promise (starts fetching during render)
function App() {
  const userPromise = fetchUser(); // starts immediately
  return (
    <Suspense fallback={<Loading />}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  );
}

并发渲染模式

并发模式:useTransition 用于非紧急标签切换(在重型内容渲染时保持导航响应)。useSyncExternalStore 在并发模式下安全订阅外部存储(浏览器 API、Redux、Zustand)——它为客户端和服务器提供快照函数(SSR 安全)。切勿在渲染中直接使用外部可变状态(撕裂风险);始终通过 useSyncExternalStore。三个参数:subscribe(返回清理)、getSnapshot(当前值)、getServerSnapshot(SSR 初始值)。这确保并发渲染期间的一致读取。Redux 和 Zustand 等库内部使用此 hook。

react
// Pattern 1: Deferred search with transition
function App() {
  const [tab, setTab] = useState("home");
  const [isPending, startTransition] = useTransition();

  return (
    <>
      <nav>
        <button
          onClick={() => startTransition(() => setTab("analytics"))}
          disabled={isPending}
        >
          {isPending ? "Loading..." : "Analytics"}
        </button>
      </nav>
      {tab === "home" && <Home />}
      {tab === "analytics" && <HeavyAnalytics />}
    </>
  );
}

// Pattern 2: useSyncExternalStore for external state
function useOnlineStatus() {
  return useSyncExternalStore(
    (callback) => {
      window.addEventListener("online", callback);
      window.addEventListener("offline", callback);
      return () => {
        window.removeEventListener("online", callback);
        window.removeEventListener("offline", callback);
      };
    },
    () => navigator.onLine, // client snapshot
    () => true // server snapshot (SSR)
  );
}
19

Context API 深入解析

createContext 与 Provider

createContext 创建一个带默认值的 context 对象,当未找到 Provider 时使用默认值。Provider 的 value prop 被所有后代消费。将 value 包裹在 useCallback/useMemo 中以防止不必要的重新渲染。

react
const ThemeContext = React.createContext({ theme: 'light', toggle: () => {} });

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  const toggle = useCallback(() => setTheme((t) => (t === 'light' ? 'dark' : 'light')), []);
  return (
    <ThemeContext.Provider value={{ theme, toggle }}>
      {children}
    </ThemeContext.Provider>
  );
}

useContext

useContext 读取最近的 Provider 值,并在该值变化时重新渲染组件。为不同关注点嵌套多个 Provider。按更新频率拆分 context 以提高性能。

react
function ThemedButton() {
  const { theme, toggle } = useContext(ThemeContext);
  return (
    <button onClick={toggle} style={{ background: theme === 'dark' ? '#333' : '#eee' }}>
      Toggle Theme
    </button>
  );
}

Context 与 Reducer

将 useReducer 与 Context 配对创建无 Redux 的全局存储。reducer 集中状态逻辑;Context 分发状态和 dispatch。消费者可以分发 action 而无需 prop 逐层传递。

react
const StoreContext = React.createContext(null);

function storeReducer(state, action) {
  switch (action.type) {
    case 'add': return { items: [...state.items, action.item] };
    case 'remove': return { items: state.items.filter((_, i) => i !== action.index) };
    default: return state;
  }
}

function StoreProvider({ children }) {
  const [state, dispatch] = useReducer(storeReducer, { items: [] });
  return <StoreContext.Provider value={{ state, dispatch }}>{children}</StoreContext.Provider>;
}

优化 Context 渲染

当 state 和 dispatch 在同一 context 中时,每次状态变化都重新渲染所有消费者。拆分它们意味着仅 dispatch 的组件在状态变化时从不重新渲染。useReducer 的 dispatch 是稳定的。

react
const StateContext = React.createContext(null);
const DispatchContext = React.createContext(null);

function Provider({ children }) {
  const [state, dispatch] = useReducer(reducer, initial);
  return (
    <StateContext.Provider value={state}>
      <DispatchContext.Provider value={dispatch}>
        {children}
      </DispatchContext.Provider>
    </StateContext.Provider>
  );
}

Context 的自定义 Hook

将 useContext 包裹在自定义 hook 中提供干净的 API 和 Provider 缺失时的清晰错误。同时导出 Provider 和 hook。这是消费 context 的推荐方式。

react
export function useAuth() {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error('useAuth must be used within AuthProvider');
  return ctx;
}
20

useReducer

基础 useReducer

useReducer 是复杂状态逻辑的 useState 替代方案。reducer 是纯函数:(state, action) => newState。dispatch 是稳定的,因此可以传递下去而无需担心重新渲染。

react
function reducer(state, action) {
  switch (action.type) {
    case 'increment': return { count: state.count + 1 };
    case 'decrement': return { count: state.count - 1 };
    case 'reset':     return { count: 0 };
    default: throw new Error('Unknown action: ' + action.type);
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  return <button onClick={() => dispatch({ type: 'increment' })}>{state.count}</button>;
}

惰性初始化

useReducer 的第三个参数是 init 函数,在初始渲染期间运行一次。当初始状态计算昂贵或希望 reset 返回计算状态时很有用。

react
function init(initialCount) {
  return { count: initialCount, history: [] };
}

function reducer(state, action) {
  switch (action.type) {
    case 'increment': return { ...state, count: state.count + 1 };
    case 'reset':     return init(action.payload);
    default: return state;
  }
}

const [state, dispatch] = useReducer(reducer, initialCount, init);

复杂状态形状

当状态有多个相关字段时 useReducer 表现出色。每个 action 描述完整的状态转换,使逻辑比分散的 setState 调用更易跟踪。保持 reducer 纯净。

react
const initialState = { users: [], loading: false, error: null, filter: 'all' };

function reducer(state, action) {
  switch (action.type) {
    case 'fetch-start': return { ...state, loading: true, error: null };
    case 'fetch-success': return { ...state, loading: false, users: action.users };
    case 'fetch-error': return { ...state, loading: false, error: action.error };
    case 'set-filter': return { ...state, filter: action.filter };
    default: return state;
  }
}

Reducer 与 Context

将 useReducer 与 Context 结合创建轻量级类 Redux 存储。reducer 持有逻辑;Context 分发状态和 dispatch。这是中型应用中应用级状态的推荐模式。

react
function todoReducer(state, action) {
  switch (action.type) {
    case 'add': return [...state, { id: Date.now(), text: action.text, done: false }];
    case 'toggle': return state.map((t) => t.id === action.id ? { ...t, done: !t.done } : t);
    case 'delete': return state.filter((t) => t.id !== action.id);
    default: return state;
  }
}

export function TodoProvider({ children }) {
  const [todos, dispatch] = useReducer(todoReducer, []);
  return <TodoContext.Provider value={{ todos, dispatch }}>{children}</TodoContext.Provider>;
}

Action 类型与模式

将 action 类型定义为常量以避免拼写错误并启用 IDE 自动补全。action 形状 { type, payload? } 是常见约定。对于 TypeScript,定义 action 类型的可辨识联合。

react
const ACTIONS = { ADD: 'add', UPDATE: 'update', DELETE: 'delete' };

function reducer(state, action) {
  switch (action.type) {
    case ACTIONS.ADD:
      return [...state, { id: action.id, ...action.payload }];
    case ACTIONS.UPDATE:
      return state.map((item) => item.id === action.id ? { ...item, ...action.payload } : item);
    case ACTIONS.DELETE:
      return state.filter((item) => item.id !== action.id);
    default: return state;
  }
}
21

useMemo 与 useCallback

useMemo

useMemo 缓存计算结果,仅在依赖变化时重新计算。用于昂贵计算(排序、过滤大数组)。依赖数组必须包含回调使用的所有内容。

react
function ProductList({ products, filter }) {
  const filtered = useMemo(() => {
    return products.filter((p) => p.category === filter);
  }, [products, filter]);

  const sorted = useMemo(() => [...filtered].sort((a, b) => a.price - b.price), [filtered]);
  return <ul>{sorted.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;
}

useCallback

useCallback 记忆化函数,使其跨渲染保持相同标识,除非依赖变化。这在将回调传递给记忆化子组件时至关重要——没有它,子组件每次都重新渲染。

react
function Parent() {
  const [count, setCount] = useState(0);
  const handleClick = useCallback(() => setCount((c) => c + 1), []);
  return <MemoizedChild onClick={handleClick} />;
}

React.memo

React.memo 包裹组件使其仅在 props 变化时重新渲染(浅比较)。第二个参数是返回 true 以跳过重新渲染的自定义比较器。将 memo 与 useCallback/useMemo 结合用于 props。

react
const ExpensiveItem = React.memo(function ExpensiveItem({ value, onClick }) {
  return <li onClick={onClick}>{value}</li>;
});

// With custom comparison
const DeepChild = React.memo(
  ({ user }) => <div>{user.name}</div>,
  (prev, next) => prev.user.id === next.user.id
);

何时记忆化

记忆化有成本,可能超过节省。仅在以下情况记忆化:(1) 计算昂贵,(2) 值传递给记忆化子组件,或 (3) 值用作 useEffect/useMemo 的依赖。

react
// GOOD: expensive computation
const sorted = useMemo(() => heavySort(data), [data]);

// GOOD: callback passed to memoized child
const onSelect = useCallback((id) => setSelected(id), []);

// BAD: cheap operation, no perf issue
const label = useMemo(() => first + ' ' + last, [first, last]);

useMemo 用于引用相等

useMemo 确保对象和数组跨渲染保持相同引用,当它们用作 useEffect/useMemo/useCallback 的依赖时很重要。没有它,{ q: query } 每次渲染都创建新对象。

react
function Search({ query }) {
  const params = useMemo(() => ({ q: query, limit: 10 }), [query]);
  useEffect(() => {
    api.search(params).then(setData);
  }, [params]); // Without useMemo, this fires every render
}
22

Portals 与 Refs

createPortal

createPortal 将子元素渲染到父组件 DOM 树之外的 DOM 节点(通常是 document.body)。这对必须逃离父层叠上下文的模态框、工具提示和下拉菜单至关重要。

react
import { createPortal } from 'react-dom';

function Modal({ open, onClose, children }) {
  if (!open) return null;
  return createPortal(
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal" onClick={(e) => e.stopPropagation()}>{children}</div>
    </div>,
    document.body
  );
}

useRef

useRef 返回一个可变对象,其 .current 跨渲染持久化而不导致重新渲染。主要用途是访问 DOM 节点。它也存储不影响 UI 的可变值。

react
function FocusInput() {
  const inputRef = useRef(null);
  const focus = () => inputRef.current?.focus();
  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={focus}>Focus</button>
    </>
  );
}

forwardRef

forwardRef 让父组件通过包裹组件将 ref 传递给子 DOM 节点。没有它,React 禁止将 ref 作为 prop 传递。ref 是包裹函数的第二个参数。

react
const FancyInput = React.forwardRef(function FancyInput({ label, ...props }, ref) {
  return (
    <label>{label}<input ref={ref} {...props} className="fancy-input" /></label>
  );
});

useImperativeHandle

useImperativeHandle 自定义通过 ref 暴露给父组件的实例——而非原始 DOM 节点,父组件仅看到你定义的方法。谨慎使用;尽可能优先使用声明式 props。

react
const VideoPlayer = React.forwardRef(function VideoPlayer(props, ref) {
  const videoRef = useRef(null);
  useImperativeHandle(ref, () => ({
    play: () => videoRef.current?.play(),
    pause: () => videoRef.current?.pause(),
    seek: (time) => { if (videoRef.current) videoRef.current.currentTime = time; },
  }));
  return <video ref={videoRef} src={props.src} />;
});

可变值的 Refs

useRef 存储不应触发重新渲染的可变值——如计时器 ID、WebSocket 实例或 'is mounted' 标志。始终在 useEffect 清理函数中清理副作用。

react
function Stopwatch() {
  const [seconds, setSeconds] = useState(0);
  const intervalRef = useRef(null);
  const start = () => {
    if (intervalRef.current) return;
    intervalRef.current = setInterval(() => setSeconds((s) => s + 1), 1000);
  };
  const stop = () => { clearInterval(intervalRef.current); intervalRef.current = null; };
  useEffect(() => () => clearInterval(intervalRef.current), []);
  return <>{seconds}<button onClick={start}>Start</button><button onClick={stop}>Stop</button></>;
}
23

错误边界

类错误边界

错误边界是类组件,在渲染期间捕获子组件树中的错误。getDerivedStateFromError 更新状态以渲染回退;componentDidCatch 记录错误。它们不捕获事件处理程序或异步代码中的错误。

react
class ErrorBoundary extends React.Component {
  constructor(props) { super(props); this.state = { hasError: false, error: null }; }
  static getDerivedStateFromError(error) { return { hasError: true, error }; }
  componentDidCatch(error, info) { console.error('Caught:', error, info); }
  render() {
    if (this.state.hasError) return this.props.fallback || <h1>Something went wrong.</h1>;
    return this.props.children;
  }
}

使用错误边界

策略性地放置错误边界,使 UI 一部分的崩溃不会拖垮整个应用。小部件周围的细粒度边界让应用其余部分继续工作。

react
function App() {
  return (
    <ErrorBoundary fallback={<ErrorPage />}>
      <Header />
      <ErrorBoundary fallback={<SidebarCrash />}><Sidebar /></ErrorBoundary>
      <ErrorBoundary fallback={<ContentCrash />}><MainContent /></ErrorBoundary>
    </ErrorBoundary>
  );
}

重置错误状态

错误边界保持错误状态直到其状态变化。提供 '重试' 按钮将 hasError 重置为 false,让 React 重新渲染子组件。更改 key prop 也会重置它。

react
class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null };
  static getDerivedStateFromError(error) { return { hasError: true, error }; }
  reset = () => this.setState({ hasError: false, error: null });
  render() {
    if (this.state.hasError) return <div><p>Failed.</p><button onClick={this.reset}>Try again</button></div>;
    return this.props.children;
  }
}

react-error-boundary 库

react-error-boundary 库提供精致的、对 hook 友好的错误边界,无需编写类。FallbackComponent 接收 error 和 resetErrorBoundary 函数。resetKeys 在这些值变化时自动重置。

react
import { ErrorBoundary } from 'react-error-boundary';

<ErrorBoundary
  FallbackComponent={ErrorFallback}
  onError={(error, info) => logError(error, info)}
  onReset={() => window.location.reload()}
  resetKeys={[location.pathname]}
>
  <Routes />
</ErrorBoundary>

异步错误处理

错误边界不捕获 promise、setTimeout 或事件处理程序中的错误。要将异步错误呈现给边界,将错误存储在状态中并在渲染期间重新抛出。边界然后捕获它。

react
function AsyncComponent() {
  const [state, setState] = useState({ data: null, error: null });
  useEffect(() => {
    let active = true;
    fetchData()
      .then((data) => { if (active) setState({ data, error: null }); })
      .catch((error) => { if (active) setState({ data: null, error }); });
    return () => { active = false; };
  }, []);
  if (state.error) throw state.error;
  if (!state.data) return <Loading />;
  return <div>{state.data}</div>;
}
24

性能优化

虚拟化

虚拟化仅渲染长列表的可见行,显著减少 DOM 节点。对超过 1000 项的列表至关重要——没有它,浏览器会被数万个节点阻塞。

react
import { useVirtualizer } from '@tanstack/react-virtual';

function BigList({ items }) {
  const parentRef = useRef(null);
  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50,
  });
  return (
    <div ref={parentRef} style={{ height: 600, overflow: 'auto' }}>
      <div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
        {virtualizer.getVirtualItems().map((vi) => (
          <div key={vi.key} style={{ position: 'absolute', top: vi.start, height: vi.size }}>
            {items[vi.index].name}
          </div>
        ))}
      </div>
    </div>
  );
}

代码分割

代码分割将包拆分为按需加载的 chunk。影响最大的分割是路由级(每个页面是单独 chunk)和重型小部件(图表库、编辑器)。用包分析器测量。

react
import { lazy, Suspense } from 'react';
const Admin = lazy(() => import('./Admin'));
const Chart = lazy(() => import('./Chart'));

function Page({ showChart }) {
  return (
    <Suspense fallback={<Skeleton />}>
      {showChart && <Chart data={data} />}
    </Suspense>
  );
}

使用 DevTools 性能分析

React DevTools Profiler 记录渲染时间并显示哪些组件重新渲染了以及原因。查找 props 实际未变化的'浪费'渲染——这些是 React.memo 的候选。

react
// Use React DevTools Profiler to record renders
// Look for:
// - Components rendering too often
// - Long commit phases
// - Wasted renders (props didn't change)

// Wrap expensive renders to find bottlenecks
function MyComponent({ data }) {
  console.time('render');
  const result = heavyCompute(data);
  console.timeEnd('render');
  return <div>{result}</div>;
}

useDeferredValue

useDeferredValue 延迟更新值,让紧急更新(输入)先发生。昂贵渲染使用延迟值,因此不阻塞输入。isStale 让你显示细微视觉提示。

react
function Search({ query }) {
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;
  const results = useMemo(() => expensiveSearch(deferredQuery), [deferredQuery]);
  return <div style={{ opacity: isStale ? 0.7 : 1 }}>{results.map((r) => <div key={r.id}>{r.name}</div>)}</div>;
}

并发特性

React 18 并发特性在重型工作期间保持 UI 响应。useTransition 和 useDeferredValue 让 React 中断渲染以处理紧急输入。自动批处理将多个 setState 调用分组为一次重新渲染。

react
import { useTransition, useDeferredValue } from 'react';

// useTransition: mark updates as non-urgent
const [isPending, startTransition] = useTransition();
const filterResults = (q) => startTransition(() => setResults(search(q)));

// Automatic batching (React 18): state updates in promises batch automatically
fetch('/api').then(() => {
  setLoading(false);   //  \
  setData(data);       //   > single re-render
  setError(null);      //  /
});
25

测试(React Testing Library)

基础渲染与查询

render() 在模拟 DOM 中挂载组件;screen 查询它。优先按角色查询(getByRole)——它们镜像辅助技术看到页面的方式并强制可访问性。userEvent 模拟真实用户交互。

react
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

test('increments on click', async () => {
  const user = userEvent.setup();
  render(<Counter />);
  expect(screen.getByText('Count: 0')).toBeInTheDocument();
  const button = screen.getByRole('button', { name: /increment/i });
  await user.click(button);
  expect(screen.getByText('Count: 1')).toBeInTheDocument();
});

查询变体

getBy 断言元素存在(否则抛出)。queryBy 用于断言不存在(返回 null)。findBy 等待异步元素出现。All 变体处理多个匹配。

react
// getBy: throws if 0 or >1 matches (strict)
screen.getByRole('button', { name: 'Submit' });

// queryBy: returns null if 0 matches (for assertions of absence)
expect(screen.queryByText('Error')).not.toBeInTheDocument();

// findBy: returns a Promise, waits for match (async)
const element = await screen.findByText('Loaded');

// getAllBy: returns array (multiple matches)
const items = screen.getAllByRole('listitem');

触发事件

userEvent(而非较低级别的 fireEvent)是模拟交互的推荐方式——它按正确顺序触发真实用户会触发的所有事件(focus、input、keydown、click)。始终对 userEvent 方法使用 await。

react
import userEvent from '@testing-library/user-event';

test('form submission', async () => {
  const user = userEvent.setup();
  const onSubmit = vi.fn();
  render(<Form onSubmit={onSubmit} />);
  await user.type(screen.getByLabelText(/email/i), '[email protected]');
  await user.click(screen.getByRole('button', { name: /submit/i }));
  expect(onSubmit).toHaveBeenCalled();
});

waitFor 与异步

waitFor 轮询直到断言通过或超时。findBy* 结合 waitFor 和 getBy 用于常见的'等待此出现'情况。within 将查询范围限定到特定元素。

react
import { waitFor, within } from '@testing-library/react';

test('shows data after fetch', async () => {
  render(<UserList />);
  await waitFor(() => {
    const list = screen.getByRole('list');
    expect(within(list).getAllByRole('listitem')).toHaveLength(3);
  });
});

// findBy is often cleaner than waitFor + getBy
test('shows data (cleaner)', async () => {
  render(<UserList />);
  expect(await screen.findAllByRole('listitem')).toHaveLength(3);
});

模拟与设置

MSW (Mock Service Worker) 在 service-worker 级别拦截网络请求,因此你的 fetch 代码无需修改即可运行。按测试设置处理程序,测试之间重置,全部完成后关闭。

react
import { rest } from 'msw';
import { setupServer } from 'msw/node';

const server = setupServer(
  rest.get('/api/user', (req, res, ctx) => res(ctx.json({ name: 'Alice' })))
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

test('shows user name', async () => {
  render(<UserProfile />);
  expect(await screen.findByText('Alice')).toBeInTheDocument();
});

这篇内容对您有帮助吗?

学习路径

从零开始学习

通过结构化课程从头学习这个语言。