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)} />
十、模式演进总结
选择建议:
- 逻辑复用 → Custom Hook(首选)
- 组件库 API 设计 → Compound Components + State Reducer
- 跨层级数据传递 → Provider 模式
- 权限守卫等拦截场景 → HOC
- 灵活渲染控制 → Render Props(特定场景)