React 中常用的设计模式有哪些?

一、是什么

设计模式是在特定场景下被反复验证的解决方案。React 的设计模式随着框架演进经历了巨大变化——从 class 组件时代的 HOC、Render Props 为主,到 Hooks 时代以 Custom Hook 为核心的新范式。理解这些模式的适用场景与演进脉络,对于写出可维护、可复用的 React 代码至关重要。

二、容器组件 / 展示组件模式

这是 React 中最经典的分层模式,由 Dan Abramov 推广:

  • 容器组件(Container):负责数据获取、状态管理、业务逻辑
  • 展示组件(Presentational):负责 UI 渲染,通过 props 接收数据
// 展示组件:纯 UI,通过 props 接收数据
function UserListView({ users, isLoading, onDelete }: UserListViewProps) {
  if (isLoading) return <Spinner />;
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>
          {user.name}
          <button onClick={() => onDelete(user.id)}>删除</button>
        </li>
      ))}
    </ul>
  );
}

// 容器组件:管理数据获取和状态
function UserListContainer() {
  const [users, setUsers] = useState<User[]>([]);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => { fetchUsers().then((data) => { setUsers(data); setIsLoading(false); }); }, []);

  return <UserListView users={users} isLoading={isLoading} onDelete={handleDelete} />;
}

Hooks 时代的演进:容器逻辑往往被提取到 Custom Hook 中,容器组件本身变得更轻量。

三、Custom Hook 模式

Custom Hook 是 Hooks 时代最核心的复用模式,替代了大部分 HOC 和 Render Props 的使用场景:

function useUsers() {
  const [users, setUsers] = useState<User[]>([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    let cancelled = false;
    fetchUsers()
      .then((data) => { if (!cancelled) { setUsers(data); setIsLoading(false); } })
      .catch((err) => { if (!cancelled) { setError(err); setIsLoading(false); } });
    return () => { cancelled = true; };
  }, []);

  const deleteUser = async (id: string) => {
    await deleteUserAPI(id);
    setUsers((prev) => prev.filter((u) => u.id !== id));
  };

  return { users, isLoading, error, deleteUser };
}

// 组件只负责 UI,逻辑全在 hook 中
function UserList() {
  const { users, isLoading, error, deleteUser } = useUsers();
  if (error) return <ErrorMessage error={error} />;
  if (isLoading) return <Spinner />;
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Custom Hook 的优势:

  • 逻辑复用:多个组件共享相同的状态逻辑
  • 关注点分离:hook 负责 "做什么",组件负责 "长什么样"
  • 可测试性:hook 可以用 renderHook 独立测试
  • 可组合性:hook 之间可以互相调用,组合出复杂逻辑

四、复合组件模式(Compound Components)

多个组件协作完成一个功能,通过共享隐式状态实现组件间通信:

interface TabsContextType {
  activeTab: string;
  setActiveTab: (tab: string) => void;
}

const TabsContext = React.createContext<TabsContextType | null>(null);

function useTabsContext() {
  const context = React.useContext(TabsContext);
  if (!context) throw new Error('Tabs 子组件必须在 Tabs 内使用');
  return context;
}

function Tabs({ defaultTab, children }: { defaultTab: string; children: React.ReactNode }) {
  const [activeTab, setActiveTab] = useState(defaultTab);

  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
      <div className="tabs">{children}</div>
    </TabsContext.Provider>
  );
}

function Tab({ value, children }: { value: string; children: React.ReactNode }) {
  const { activeTab, setActiveTab } = useTabsContext();
  return (
    <button role="tab" aria-selected={activeTab === value} onClick={() => setActiveTab(value)}>
      {children}
    </button>
  );
}

function TabPanel({ value, children }: { value: string; children: React.ReactNode }) {
  const { activeTab } = useTabsContext();
  return activeTab === value ? <div role="tabpanel">{children}</div> : null;
}

// 使用:声明式 API
function App() {
  return (
    <Tabs defaultTab="profile">
      <Tabs.TabList>
        <Tabs.Tab value="profile">个人资料</Tabs.Tab>
        <Tabs.Tab value="settings">设置</Tabs.Tab>
        <Tabs.Tab value="notifications">通知</Tabs.Tab>
      </Tabs.TabList>
      <Tabs.TabPanel value="profile"><Profile /></Tabs.TabPanel>
      <Tabs.TabPanel value="settings"><Settings /></Tabs.TabPanel>
      <Tabs.TabPanel value="notifications"><Notifications /></Tabs.TabPanel>
    </Tabs>
  );
}

五、Render Props 模式

通过函数 prop 将渲染逻辑交给调用方,实现行为与表现的分离:

interface MouseTrackerProps {
  children: (position: { x: number; y: number }) => React.ReactNode;
}

function MouseTracker({ children }: MouseTrackerProps) {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handleMove = (e: MouseEvent) => {
      setPosition({ x: e.clientX, y: e.clientY });
    };
    window.addEventListener('mousemove', handleMove);
    return () => window.removeEventListener('mousemove', handleMove);
  }, []);

  return <>{children(position)}</>;
}

// 使用
<MouseTracker>
  {({ x, y }) => (
    <div>鼠标位置: ({x}, {y})</div>
  )}
</MouseTracker>

Hooks 时代,这个模式大多被 Custom Hook 替代,但在需要灵活控制渲染输出的组件库中仍有价值。

六、高阶组件模式(HOC)

HOC 是一个接收组件并返回新组件的函数,用于横切关注点的复用:

function withAuth<P extends object>(WrappedComponent: React.ComponentType<P>) {
  return function AuthenticatedComponent(props: P) {
    const { user, isLoading } = useAuth();

    if (isLoading) return <Spinner />;
    if (!user) return <Navigate to="/login" />;

    return <WrappedComponent {...props} />;
  };
}

const ProtectedDashboard = withAuth(Dashboard);

HOC 的问题:

  • Props 来源不透明:不清楚 props 从哪个 HOC 注入
  • 嵌套地狱:多个 HOC 层层包裹难以调试
  • 命名冲突:不同 HOC 可能注入同名 prop
  • TypeScript 支持困难:类型传递复杂

Hooks 时代建议:大部分 HOC 场景都可以用 Custom Hook 替代,但对于需要拦截渲染(如权限守卫)的场景,HOC 仍然适用。

七、Provider 模式

利用 Context 实现跨层级的数据传递,避免 prop drilling:

interface ThemeContextType {
  theme: 'light' | 'dark';
  toggleTheme: () => void;
}

const ThemeContext = React.createContext<ThemeContextType | null>(null);

function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');

  const toggleTheme = useCallback(() => {
    setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
  }, []);

  const value = useMemo(() => ({ theme, toggleTheme }), [theme, toggleTheme]);

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) throw new Error('useTheme 必须在 ThemeProvider 内使用');
  return context;
}

注意使用 useMemo 包裹 value 避免 Provider 不必要的重渲染。

八、State Reducer 模式

允许调用方通过自定义 reducer 覆盖组件内部的状态更新逻辑:

type ToggleAction = { type: 'TOGGLE' } | { type: 'ON' } | { type: 'OFF' };

interface ToggleState {
  isOn: boolean;
}

function defaultReducer(state: ToggleState, action: ToggleAction): ToggleState {
  switch (action.type) {
    case 'TOGGLE': return { isOn: !state.isOn };
    case 'ON': return { isOn: true };
    case 'OFF': return { isOn: false };
  }
}

function useToggle(
  reducer: (state: ToggleState, action: ToggleAction) => ToggleState = defaultReducer
) {
  const [state, dispatch] = useReducer(reducer, { isOn: false });
  const toggle = () => dispatch({ type: 'TOGGLE' });
  const setOn = () => dispatch({ type: 'ON' });
  const setOff = () => dispatch({ type: 'OFF' });

  return { isOn: state.isOn, toggle, setOn, setOff };
}

// 使用方自定义行为:例如限制最多切换 3 次
const { isOn, toggle } = useToggle((state, action) => {
  if (action.type === 'TOGGLE' && countRef.current >= 3) return state;
  if (action.type === 'TOGGLE') countRef.current++;
  return defaultReducer(state, action);
});

这种模式给了使用方对组件行为的控制反转能力,在组件库开发中非常实用。

九、受控 / 非受控模式

同一组件同时支持受控和非受控两种使用方式:

interface InputProps {
  value?: string;           // 受控模式
  defaultValue?: string;    // 非受控模式
  onChange?: (value: string) => void;
}

function CustomInput({ value, defaultValue, onChange }: InputProps) {
  const [internalValue, setInternalValue] = useState(defaultValue ?? '');
  const isControlled = value !== undefined;
  const currentValue = isControlled ? value : internalValue;

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const newValue = e.target.value;
    if (!isControlled) {
      setInternalValue(newValue);
    }
    onChange?.(newValue);
  };

  return <input value={currentValue} onChange={handleChange} />;
}

// 受控用法
<CustomInput value={name} onChange={setName} />

// 非受控用法
<CustomInput defaultValue="初始值" onChange={(v) => console.log(v)} />

十、模式演进总结

模式Class 时代Hooks 时代推荐程度
Container/Presentational核心模式仍有价值,容器逻辑迁移到 hook
Custom Hook核心复用模式
Compound ComponentsContext + cloneElementContext + hooks
Render Props主流复用方案大部分被 hook 替代
HOC主流复用方案仅限拦截渲染场景
ProviderContext APIContext + useMemo 优化
State ReduceruseReducer 实现组件库常用
受控/非受控一直通用一直通用

选择建议:

  • 逻辑复用 → Custom Hook(首选)
  • 组件库 API 设计 → Compound Components + State Reducer
  • 跨层级数据传递 → Provider 模式
  • 权限守卫等拦截场景 → HOC
  • 灵活渲染控制 → Render Props(特定场景)