引言
在 React 开发中,组件复用是核心课题。昨天我们学习了受控/非受控组件和容器组件模式,今天继续深入三种高级设计模式:高阶组件( HOC ) 、Render Props 和 Compound Components。掌握这些模式,能让你写出更灵活、可维护的代码。
一、高阶组件(HOC)
HOC 是接收组件并返回新组件的函数,本质是函数式编程的组合思想。
典型场景:权限控制
javascript
// withAuth.jsx
function withAuth(WrappedComponent) {
return function AuthenticatedComponent(props) {
const { isAuthenticated, user } = useAuth();
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
return <WrappedComponent {...props} user={user} />;
};
}
// 使用
const ProtectedDashboard = withAuth(Dashboard);
典型场景:日志追踪
javascript
// withLogging.jsx
function withLogging(WrappedComponent) {
return function LoggedComponent(props) {
useEffect(() => {
console.log(`[Mount] ${WrappedComponent.name}`, props);
return () => {
console.log(`[Unmount] ${WrappedComponent.name}`);
};
}, [props]);
return <WrappedComponent {...props} />;
};
}
⚠️ HOC 注意事项
- 不要在 render 中使用 HOC:会导致组件树重建,状态丢失
- 静态方法 丢失 :需要用
hoist-non-react-statics拷贝 - ref 传递问题 :需要用
forwardRef处理
二、Render Props 模式
Render Props 是通过函数 prop 实现代码复用的模式,比 HOC 更灵活。
基础示例:鼠标追踪
javascript
// MouseTracker.jsx
function MouseTracker({ children }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMove = (e) => {
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>
实战:数据请求 Render Props
javascript
// Fetch.jsx
function Fetch({ url, children, onError }) {
const [state, setState] = useState({
data: null,
loading: true,
error: null
});
useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then(res => res.json())
.then(data => setState({ data, loading: false, error: null }))
.catch(err => {
if (err.name !== 'AbortError') {
setState({ data: null, loading: false, error: err });
onError?.(err);
}
});
return () => controller.abort();
}, [url]);
return children(state);
}
// 使用
<Fetch url="/api/users">
{({ data, loading, error }) => {
if (loading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
return <UserList users={data} />;
}}
</Fetch>
Render Props vs HOC
| 维度 | HOC | Render Props |
|---|---|---|
| 嵌套层级 | 易产生包装地狱 | 更扁平 |
| prop 冲突 | 可能覆盖原 prop | 无冲突 |
| 类型推断 | 较复杂 | 更直观 |
| 使用场景 | 简单增强 | 复杂逻辑复用 |
三、Compound Components 模式
Compound Components 通过上下文实现组件间隐式通信,适合构建复杂 UI 组件。
实战:可组合的 Select 组件
javascript
// Select.jsx
const SelectContext = createContext(null);
function Select({ children, value, onChange }) {
const [open, setOpen] = useState(false);
return (
<SelectContext.Provider value={{ value, onChange, open, setOpen }}>
<div className="select-container">{children}</div>
</SelectContext.Provider>
);
}
function SelectTrigger({ children }) {
const { open, setOpen } = useContext(SelectContext);
return (
<button onClick={() => setOpen(!open)} className="trigger">
{children}
</button>
);
}
function SelectContent({ children }) {
const { open } = useContext(SelectContext);
if (!open) return null;
return <div className="content">{children}</div>;
}
function SelectOption({ value, children }) {
const { value: selectedValue, onChange } = useContext(SelectContext);
const isSelected = value === selectedValue;
return (
<div
className={`option ${isSelected ? 'selected' : ''}`}
onClick={() => onChange(value)}
>
{children}
</div>
);
}
// 组合使用
Select.Trigger = SelectTrigger;
Select.Content = SelectContent;
Select.Option = SelectOption;
// 实际使用
<Select value={selected} onChange={setSelected}>
<Select.Trigger>选择城市</Select.Trigger>
<Select.Content>
<SelectOption value="beijing">北京</SelectOption>
<SelectOption value="shanghai">上海</SelectOption>
<SelectOption value="guangzhou">广州</SelectOption>
</Select.Content>
</Select>
优势分析
- 声明式 API:使用者只需关注组合,不关心内部实现
- 灵活布局:子组件可以任意排列,不受父组件限制
- 隐式通信:通过 Context 传递状态,避免 prop drilling
总结
| 模式 | 核心思想 | 适用场景 |
|---|---|---|
| HOC | 函数组合增强 | 权限、日志、数据注入 |
| Render Props | 函数 prop 复用 | 复杂逻辑、动态渲染 |
| Compound | Context 隐式通信 | 复杂 UI 组件库 |
选型建议:
- 简单功能增强 → HOC
- 需要灵活渲染 → Render Props
- 构建组件库 → Compound Components
React 18 之后,许多场景可以用 Hooks + Context 替代,但理解这些经典模式对阅读老代码和设计复杂组件仍有重要价值。