- 复合组件为组件库提供灵活且声明式的 API
- 自定义 Hook 是现代 React 中最通用的逻辑复用模式
- Provider 模式通过 Context + Hook 实现简洁的依赖注入
- useReducer 自然地建模有限状态机,防止不可能的状态
- 优先选择组合而非继承——使用 children、render props 和 slot 模式
- Error Boundary 隔离故障,防止整个应用崩溃
1. 复合组件模式
复合组件模式允许父组件通过隐式状态与子组件通信。父组件管理内部状态,子组件通过 Context 或 React.Children 访问该状态。这种模式常见于 Select/Option、Tabs/Tab 和 Accordion 等组件库中,它使 API 保持声明式且灵活。
与通过 props 传递大量配置不同,复合组件让消费者通过组合子组件来声明意图。这种方式更接近原生 HTML(如 <select> + <option>),降低了使用门槛。
示例:Select 和 Option
function Select({ children, onChange }) {
const [value, setValue] = React.useState(null);
const handleSelect = (val) => {
setValue(val);
onChange?.(val);
};
return (
<SelectContext.Provider value={{ value, onSelect: handleSelect }}>
<div role="listbox">{children}</div>
</SelectContext.Provider>
);
}
function Option({ value, children }) {
const ctx = React.useContext(SelectContext);
const selected = ctx.value === value;
return (
<div role="option" onClick={() => ctx.onSelect(value)}
style={{ fontWeight: selected ? "bold" : "normal" }}>
{children}
</div>
);
}2. Render Props 模式
Render Props 是一种通过函数 prop 共享渲染逻辑的技术。组件接收一个返回 JSX 的函数作为 prop,将内部状态传递给该函数。这种模式适用于数据获取、鼠标追踪和交叉观察等需要将逻辑与渲染解耦的场景。
虽然自定义 Hook 在很多场景下已取代 Render Props,但当你需要根据共享逻辑动态决定渲染内容时,Render Props 仍然有其独特价值。
示例:鼠标位置追踪
function MouseTracker({ render }) {
const [pos, setPos] = React.useState({ x: 0, y: 0 });
React.useEffect(() => {
const handler = (e) => setPos({ x: e.clientX, y: e.clientY });
window.addEventListener("mousemove", handler);
return () => window.removeEventListener("mousemove", handler);
}, []);
return render(pos);
}
// Usage:
<MouseTracker
render={({ x, y }) => (
<p>Cursor at: {x}, {y}</p>
)}
/>3. 自定义 Hook 模式
自定义 Hook 是现代 React 中最推荐的逻辑复用模式。将有状态逻辑封装到以 use 开头的函数中,可以在多个组件间共享而不改变组件层级。常见的自定义 Hook 包括 useDebounce、useLocalStorage 和 useMediaQuery。
自定义 Hook 的优势在于:它们可以调用其他 Hook、返回任意值、并且不会产生额外的组件嵌套。每个使用自定义 Hook 的组件都有独立的状态副本。
示例:useDebounce 和 useLocalStorage
function useDebounce(value, delay = 300) {
const [debounced, setDebounced] = React.useState(value);
React.useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(id);
}, [value, delay]);
return debounced;
}
function useLocalStorage(key, initial) {
const [val, setVal] = React.useState(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initial;
});
React.useEffect(() => {
localStorage.setItem(key, JSON.stringify(val));
}, [key, val]);
return [val, setVal];
}4. 高阶组件 (HOC)
高阶组件是一个接收组件并返回新组件的函数,为原组件添加额外功能。常见的 HOC 包括 withAuth(鉴权)、withLoading(加载状态)和 withTheme(主题注入)。多个 HOC 可以通过 compose 函数组合使用。
HOC 的缺点包括:可能导致 prop 名冲突、使组件树变深导致调试困难、以及静态方法不会自动传递。在现代 React 中,优先使用自定义 Hook,仅在需要包裹渲染逻辑时使用 HOC。
示例:withAuth 高阶组件
function withAuth(WrappedComponent) {
return function AuthenticatedComponent(props) {
const { user, loading } = useAuth();
if (loading) return <Spinner />;
if (!user) return <Redirect to="/login" />;
return <WrappedComponent {...props} user={user} />;
};
}
// Compose multiple HOCs:
const compose = (...fns) =>
fns.reduce((f, g) => (...args) => f(g(...args)));
const EnhancedPage = compose(
withAuth,
withTheme,
withLoading
)(DashboardPage);5. 容器组件/展示组件
容器/展示组件模式将数据逻辑与 UI 渲染分离。容器组件负责获取数据、管理状态和处理副作用,展示组件只接收 props 并渲染 UI。虽然 Hook 减少了对此模式的依赖,但在大型代码库中它仍然有助于保持清晰的关注点分离。
展示组件通常是纯函数组件,容易测试和复用。容器组件可以替换为自定义 Hook,但这种分离思想仍然有价值——将"做什么"与"怎么呈现"分开。
示例:用户列表分离
// Container: handles data and state
function UserListContainer() {
const [users, setUsers] = React.useState([]);
React.useEffect(() => {
fetch("/api/users").then(r => r.json()).then(setUsers);
}, []);
return <UserListView users={users} />;
}
// Presentational: pure UI rendering
function UserListView({ users }) {
return (
<ul>
{users.map(u => (
<li key={u.id}>{u.name} — {u.email}</li>
))}
</ul>
);
}6. Provider 模式
Provider 模式将 React Context 与自定义 Hook 结合,实现简洁的依赖注入。Provider 组件包裹组件树的一部分,使值对所有后代组件可用。自定义 Hook(如 useTheme)消费该 Context,避免了 prop drilling 的问题。
这是 Redux、Zustand、React Query 等状态管理库的基础架构。在自定义 Hook 中添加错误检查(确保在 Provider 内使用)是一个最佳实践,能在开发时提供清晰的错误信息。
示例:主题 Provider
const ThemeContext = React.createContext(null);
function ThemeProvider({ children }) {
const [theme, setTheme] = React.useState("light");
const toggle = () => setTheme(t => t === "light" ? "dark" : "light");
return (
<ThemeContext.Provider value={{ theme, toggle }}>
{children}
</ThemeContext.Provider>
);
}
function useTheme() {
const ctx = React.useContext(ThemeContext);
if (!ctx) throw new Error("useTheme must be inside ThemeProvider");
return ctx;
}7. 状态机模式
状态机模式使用有限状态和明确的转换规则来管理复杂 UI 逻辑。useReducer 天然适合建模状态机,因为每个 action 代表一个有效的状态转换。这种模式可以防止不可能的状态,使多步表单、异步工作流和复杂交互更容易推理和测试。
对于更复杂的状态机需求,可以使用 XState 库,它提供可视化调试工具和更严格的类型安全。简单场景下,useReducer 配合 switch 语句已经足够。
示例:异步请求状态机
function fetchReducer(state, action) {
switch (state.status) {
case "idle":
if (action.type === "FETCH") return { status: "loading" };
break;
case "loading":
if (action.type === "SUCCESS")
return { status: "success", data: action.data };
if (action.type === "ERROR")
return { status: "error", error: action.error };
break;
case "error":
if (action.type === "RETRY") return { status: "loading" };
break;
}
return state;
}
const [state, dispatch] = React.useReducer(
fetchReducer, { status: "idle" }
);8. 受控组件 vs 非受控组件
受控组件将表单值存储在 React 状态中,通过 onChange 更新;非受控组件让 DOM 管理值,通过 ref 读取。受控组件适合需要验证、条件渲染或派生状态的场景;非受控组件适合简单表单、文件输入或与非 React 库集成。
React.forwardRef 和 useImperativeHandle 可以让父组件通过 ref 访问非受控组件的内部方法,这在需要命令式操作(如聚焦输入框或重置表单)时非常有用。
示例:受控与非受控对比
// Controlled: React owns the value
function ControlledInput() {
const [value, setValue] = React.useState("");
return (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
/>
);
}
// Uncontrolled: DOM owns the value
const UncontrolledInput = React.forwardRef((props, ref) => {
const inputRef = React.useRef(null);
React.useImperativeHandle(ref, () => ({
getValue: () => inputRef.current.value,
}));
return <input ref={inputRef} defaultValue={props.initial} />;
});9. 组合 vs 继承
React 官方推荐组合而非继承。通过 children prop、render props 和 slot 模式,可以在不创建紧耦合的情况下自定义组件行为。在 Facebook 数千个 React 组件中,没有发现继承优于组合的场景。
Slot 模式通过命名 props 传递 JSX 片段,提供了类似 Vue slot 的能力。这比继承更灵活,因为每个"槽位"可以是任意 JSX,而继承只能沿类层次结构扩展。
示例:Slot 模式
// Slots pattern: named regions via props
function Card({ header, body, footer }) {
return (
<div style={{ border: "1px solid #ccc", borderRadius: 8 }}>
<div style={{ padding: 16, borderBottom: "1px solid #eee" }}>
{header}
</div>
<div style={{ padding: 16 }}>{body}</div>
{footer && (
<div style={{ padding: 16, borderTop: "1px solid #eee" }}>
{footer}
</div>
)}
</div>
);
}
// Usage: fully customizable without inheritance
<Card
header={<h3>Title</h3>}
body={<p>Content here</p>}
footer={<button>Save</button>}
/>10. 观察者模式
观察者模式(发布/订阅)在 React 中用于解耦组件间的通信。通过事件发射器,组件可以发布事件而不需要知道谁在监听,订阅者也不需要知道事件来自哪里。这在跨组件树通信和与外部系统集成时非常有用。
在 React 中使用观察者模式时,务必在 useEffect 的清理函数中取消订阅,避免内存泄漏。对于简单的跨组件通信,Context 或状态管理库通常是更好的选择。
示例:事件发射器
function createEventBus() {
const listeners = new Map();
return {
on(event, cb) {
if (!listeners.has(event)) listeners.set(event, new Set());
listeners.get(event).add(cb);
return () => listeners.get(event).delete(cb);
},
emit(event, data) {
listeners.get(event)?.forEach(cb => cb(data));
},
};
}
// Usage in a hook:
function useEventBus(bus, event, handler) {
React.useEffect(() => bus.on(event, handler),
[bus, event, handler]);
}11. 代理模式
代理模式在 React 中用于延迟加载和虚拟化。代理组件作为真实组件的替代,在满足条件时(如进入视口)才加载实际内容。React.lazy 和 Intersection Observer 是实现代理模式的常用工具。
虚拟滚动也是代理模式的一种应用:只渲染视口内的列表项,用占位元素代替不可见的部分。这对包含数千条目的长列表至关重要。
示例:视口延迟加载
function LazyVisible({ children, fallback = null }) {
const ref = React.useRef(null);
const [visible, setVisible] = React.useState(false);
React.useEffect(() => {
const obs = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
setVisible(true);
obs.disconnect();
}
});
if (ref.current) obs.observe(ref.current);
return () => obs.disconnect();
}, []);
return (
<div ref={ref}>
{visible ? children : fallback}
</div>
);
}12. 模块模式
模块模式在 React 中体现为桶导出、功能模块和代码分割。通过将相关组件、Hook 和工具函数组织在同一目录下,使用 index.ts 作为公共 API,可以保持代码整洁且易于维护。React.lazy 和动态 import 实现按需加载。
功能模块的好处是:团队成员可以独立开发各自负责的模块,模块间通过明确的公共 API 交互,内部实现细节被封装起来。这也使得代码分割的边界更加清晰。
示例:功能模块结构
// features/auth/index.ts — barrel export
export { AuthProvider } from "./AuthProvider";
export { useAuth } from "./useAuth";
export { LoginForm } from "./LoginForm";
export { SignupForm } from "./SignupForm";
// Lazy loading a feature module:
const AuthModule = React.lazy(() => import("./features/auth"));
function App() {
return (
<React.Suspense fallback={<Spinner />}>
<AuthModule />
</React.Suspense>
);
}13. Error Boundary 模式
Error Boundary 是捕获子组件树中 JavaScript 错误的类组件,显示降级 UI 而不是让整个应用崩溃。它们使用 componentDidCatch 和 getDerivedStateFromError 生命周期方法。在不同的 UI 区域使用多个 Error Boundary 可以隔离故障。
Error Boundary 只能捕获渲染期间、生命周期方法和构造函数中的错误。它们无法捕获事件处理器、异步代码或服务端渲染中的错误。对于这些场景,需要使用 try/catch 或全局错误处理。
示例:带重试功能的 Error Boundary
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, info) {
console.error("Caught:", error, info.componentStack);
}
handleRetry = () => this.setState({ hasError: false, error: null });
render() {
if (this.state.hasError) {
return (
<div style={{ padding: 20, textAlign: "center" }}>
<h3>Something went wrong</h3>
<button onClick={this.handleRetry}>Try Again</button>
</div>
);
}
return this.props.children;
}
}模式演进:从 Mixins 到 Hooks
React 的设计模式随着框架的发展而不断演进。理解这个演进过程有助于你明白为什么某些模式被推荐,以及何时使用旧模式仍然合理。
- Mixins (2013-2015): React 最早的逻辑复用方式,仅支持 createClass。因为命名冲突和隐式依赖问题被废弃。
- HOC (2015-2018): 用函数包裹组件来注入行为。解决了 Mixins 的问题,但引入了 wrapper hell 和 prop 冲突。
- Render Props (2017-2019): 通过函数 prop 共享逻辑。比 HOC 更显式,但仍然有组件嵌套问题。
- Hooks (2019-present): 革命性的逻辑复用方案。无嵌套、无命名冲突、可组合性强。是目前推荐的首选方式。
尽管 Hook 是首选方案,但 HOC 在某些场景(如路由守卫、权限控制)中仍然有用,Render Props 在需要动态渲染决策时仍然有价值,Error Boundary 仍然必须使用类组件。
实战组合模式
在实际项目中,设计模式很少单独使用。以下是几种常见的模式组合方式:
Provider + Custom Hook + Error Boundary
这是最常见的组合。Provider 提供数据源,自定义 Hook 封装访问逻辑,Error Boundary 处理加载失败。例如一个数据获取层:AuthProvider 提供用户信息,useAuth Hook 让组件访问认证状态,Error Boundary 在 API 异常时显示降级 UI。
Compound Components + Provider + State Machine
构建复杂的表单向导时,外层使用复合组件定义步骤(Wizard + Step),内部通过 Provider 共享向导状态,用 useReducer 状态机管理步骤转换和验证逻辑。
Module Pattern + Proxy + Code Splitting
大型应用通过模块模式组织功能边界,使用代理模式(React.lazy + Suspense)按需加载模块,结合路由级代码分割实现最优的首屏加载性能。
如何测试设计模式
良好的设计模式应该使测试更容易而非更困难。以下是每种模式的测试建议:
- 自定义 Hook:使用 renderHook 从 @testing-library/react 独立测试 Hook 逻辑。
- 复合组件:将父子组件一起渲染测试,验证交互行为而非实现细节。
- Provider 模式:创建测试用的 Provider wrapper,注入模拟数据,测试消费组件的行为。
- 状态机:Reducer 是纯函数,可以脱离 React 直接单元测试每个状态转换。
- Error Boundary:故意渲染抛出错误的子组件,验证降级 UI 是否正确显示。
- 展示组件:使用快照测试或视觉回归测试,因为展示组件是纯函数。
模式选择指南
选择正确的设计模式取决于你的具体需求。以下是按用途分类的快速参考:
- 逻辑复用 → 自定义 Hook(首选)或 Render Props
- 组件库 API → 复合组件
- 全局状态/依赖注入 → Provider 模式
- 复杂状态逻辑 → 状态机 (useReducer / XState)
- 跨功能增强 → 高阶组件(少用,优先 Hook)
- 错误恢复 → Error Boundary
- 性能优化 → 代理模式(懒加载/虚拟化)
- 代码组织 → 模块模式(桶导出 + 代码分割)
- 表单处理 → 受控(验证)/ 非受控(简单场景)
- 松耦合通信 → 观察者模式(事件总线)
常见反模式
了解反模式与了解设计模式同样重要。避免以下常见陷阱可以显著提升代码质量:
- Prop Drilling:传递 props 超过 3 层时,考虑使用 Context 或状态管理库。过深的 prop 传递使组件耦合,重构困难。
- 过度抽象:不要在只有一个使用场景时就创建抽象,遵循"三次法则"——只有当模式重复三次时才提取。
- 巨型组件:超过 200 行的组件应该被拆分为更小的子组件或自定义 Hook。大组件难以测试、难以维护。
- 在 render 中定义组件:在 render 函数内部定义组件会导致每次渲染都创建新的组件实例,破坏 reconciliation 和状态保持。
- 滥用 useEffect:不要用 useEffect 来同步派生状态。如果一个值可以从 props 或 state 计算得出,直接在渲染中计算即可。
性能注意事项
设计模式不仅影响代码的可维护性,还直接影响运行时性能。以下是使用各模式时需要注意的性能问题:
- Context: 每次 Context value 变化时,所有消费者都会重新渲染。通过拆分 Context(读写分离)或使用 useMemo 包裹 value 来优化。
- 复合组件:使用 React.memo 包裹子组件,避免父组件状态变化导致所有子组件不必要地重新渲染。
- HOC: 确保 HOC 返回的组件被正确 memo 化。避免在每次渲染时创建新的 HOC 实例。
- 事件总线:频繁触发的事件(如鼠标移动)应该使用节流或防抖来限制回调执行频率。
- 代码分割:React.lazy 产生的网络请求会影响感知性能。使用预加载策略(如 hover 时预加载)减少用户等待时间。
何时不使用设计模式
设计模式是解决特定问题的工具,而非炫技的手段。在以下场景中,简单直接的代码比精心设计的模式更好:
- 原型或 MVP 阶段——快速验证想法比代码架构更重要
- 只有一个组件使用的逻辑——不需要提取自定义 Hook
- Props 只传两层——不需要引入 Context
- 状态只有两个分支——不需要状态机,简单的 useState + if/else 就够了
- 团队不熟悉某个模式——可读性和团队效率比"最佳实践"更重要
总结
React 设计模式不是互斥的——它们经常组合使用。一个典型的 React 应用可能同时使用 Provider 模式管理主题和认证、自定义 Hook 共享业务逻辑、复合组件构建可复用的 UI 组件库、Error Boundary 处理异常情况。
关键是理解每种模式解决的问题,选择最适合你场景的方案,并避免过度工程化。从简单开始,随着需求增长逐步引入更高级的模式。记住:最好的模式是你的团队能理解和维护的模式。
随着 React Server Components 和新的编译器优化(React Compiler)的引入,某些模式可能会继续演进。但核心原则不会改变:关注点分离、单一职责、组合优于继承、以及保持 API 的简洁性。掌握这些模式的本质思想,你就能在任何新框架或新范式中快速适应。
推荐学习资源
- React Official Docs: react.dev 的"Thinking in React"和"Reusing Logic with Custom Hooks"章节是理解模式思想的最佳起点。
- Patterns.dev: 由 Lydia Hallie 和 Addy Osmani 维护的免费资源,提供交互式的设计模式和渲染模式讲解。
- XState: 如果你对状态机感兴趣,XState 提供了可视化编辑器和完整的有限状态机实现。
- Testing Library: @testing-library/react 的文档展示了如何以用户为中心的方式测试各种模式。
设计模式是经验的结晶。多读开源代码(如 Radix UI、Headless UI、React Query 的源码),观察这些成熟的库如何运用这些模式,是提升架构能力的最有效方法之一。