高阶组件 #
一、高阶组件概述 #
1.1 什么是高阶组件 #
高阶组件(Higher-Order Component,HOC)是一个函数,接收一个组件并返回一个新组件。
text
Component → HOC → Enhanced Component
1.2 基本形式 #
jsx
function higherOrderComponent(WrappedComponent) {
return function EnhancedComponent(props) {
return <WrappedComponent {...props} />;
};
}
// 箭头函数形式
const withEnhancement = (WrappedComponent) => (props) => (
<WrappedComponent {...props} />
);
1.3 使用场景 #
| 场景 | 说明 |
|---|---|
| 逻辑复用 | 复用组件逻辑 |
| 属性代理 | 操作 props |
| 渲染劫持 | 控制渲染 |
| 状态抽象 | 提取状态管理 |
二、属性代理 #
2.1 基本示例 #
jsx
function withLoading(WrappedComponent) {
return function WithLoadingComponent({ isLoading, ...props }) {
if (isLoading) {
return <div>Loading...</div>;
}
return <WrappedComponent {...props} />;
};
}
// 使用
const UserListWithLoading = withLoading(UserList);
<UserListWithLoading isLoading={loading} users={users} />
2.2 添加额外 Props #
jsx
function withUser(WrappedComponent) {
return function WithUserComponent(props) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchCurrentUser().then(setUser);
}, []);
return <WrappedComponent {...props} user={user} />;
};
}
// 使用
const ProfileWithUser = withUser(Profile);
<ProfileWithUser />
2.3 操作 Props #
jsx
function withDefaultValue(WrappedComponent, defaultProps) {
return function WithDefaultPropsComponent(props) {
const mergedProps = { ...defaultProps, ...props };
return <WrappedComponent {...mergedProps} />;
};
}
// 使用
const InputWithDefault = withDefaultValue(Input, {
type: 'text',
placeholder: 'Enter value'
});
<InputWithDefault />
2.4 提取状态 #
jsx
function withFormState(WrappedComponent) {
return function WithFormStateComponent(props) {
const [values, setValues] = useState({});
const [errors, setErrors] = useState({});
const handleChange = (name, value) => {
setValues(prev => ({ ...prev, [name]: value }));
};
const setFieldError = (name, error) => {
setErrors(prev => ({ ...prev, [name]: error }));
};
return (
<WrappedComponent
{...props}
values={values}
errors={errors}
handleChange={handleChange}
setFieldError={setFieldError}
/>
);
};
}
三、渲染劫持 #
3.1 条件渲染 #
jsx
function withAuthentication(WrappedComponent) {
return function WithAuthenticationComponent(props) {
const { isAuthenticated } = useContext(AuthContext);
if (!isAuthenticated) {
return <LoginPage />;
}
return <WrappedComponent {...props} />;
};
}
// 使用
const ProtectedDashboard = withAuthentication(Dashboard);
3.2 权限控制 #
jsx
function withPermission(WrappedComponent, requiredPermission) {
return function WithPermissionComponent(props) {
const { user } = useContext(UserContext);
if (!user?.permissions?.includes(requiredPermission)) {
return <AccessDenied />;
}
return <WrappedComponent {...props} />;
};
}
// 使用
const AdminPanel = withPermission(AdminPanelComponent, 'admin');
3.3 修改渲染输出 #
jsx
function withLayout(WrappedComponent, layoutProps) {
return function WithLayoutComponent(props) {
return (
<div class="layout">
<Header {...layoutProps?.header} />
<main>
<WrappedComponent {...props} />
</main>
<Footer {...layoutProps?.footer} />
</div>
);
};
}
四、组合 HOC #
4.1 嵌套使用 #
jsx
const EnhancedComponent = withAuthentication(
withPermission(
withLoading(
UserProfile
),
'view_profile'
)
);
// 使用
<EnhancedComponent isLoading={loading} />
4.2 compose 工具函数 #
jsx
function compose(...funcs) {
return funcs.reduce((a, b) => (...args) => a(b(...args)), arg => arg);
}
// 使用
const enhance = compose(
withAuthentication,
withPermission('view_profile'),
withLoading
);
const EnhancedUserProfile = enhance(UserProfile);
4.3 使用示例 #
jsx
// 定义多个 HOC
function withLogger(WrappedComponent) {
return function WithLoggerComponent(props) {
useEffect(() => {
console.log('Component mounted/updated');
});
return <WrappedComponent {...props} />;
};
}
function withTheme(WrappedComponent) {
return function WithThemeComponent(props) {
const theme = useContext(ThemeContext);
return <WrappedComponent {...props} theme={theme} />;
};
}
function withErrorBoundary(WrappedComponent) {
return class WithErrorBoundaryComponent extends Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return <WrappedComponent {...this.props} />;
}
};
}
// 组合使用
const enhance = compose(
withErrorBoundary,
withTheme,
withLogger
);
const EnhancedComponent = enhance(MyComponent);
五、Ref 转发 #
5.1 问题 #
jsx
function withHOC(WrappedComponent) {
return function EnhancedComponent(props) {
return <WrappedComponent {...props} />;
};
}
// ref 会指向 EnhancedComponent,而非 WrappedComponent
const EnhancedInput = withHOC(Input);
<input ref={inputRef} /> // ref 不是指向 Input
5.2 解决方案 #
jsx
import { forwardRef } from 'preact';
function withHOC(WrappedComponent) {
const EnhancedComponent = forwardRef((props, ref) => {
return <WrappedComponent {...props} ref={ref} />;
});
EnhancedComponent.displayName = `withHOC(${WrappedComponent.displayName || WrappedComponent.name})`;
return EnhancedComponent;
}
六、命名规范 #
6.1 displayName #
jsx
function withHOC(WrappedComponent) {
function WithHOC(props) {
return <WrappedComponent {...props} />;
}
WithHOC.displayName = `withHOC(${getDisplayName(WrappedComponent)})`;
return WithHOC;
}
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
6.2 命名约定 #
jsx
// HOC 函数名以 with 开头
function withLoading() {}
function withAuthentication() {}
function withTheme() {}
// 返回的组件名以 With 开头或添加后缀
function withLoading(WrappedComponent) {
return function WithLoading(props) {
// ...
};
}
七、实际示例 #
7.1 withFetch #
jsx
function withFetch(url) {
return function (WrappedComponent) {
return function WithFetchComponent(props) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
async function fetchData() {
try {
setLoading(true);
const response = await fetch(url);
const json = await response.json();
if (!cancelled) {
setData(json);
}
} catch (e) {
if (!cancelled) {
setError(e);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
fetchData();
return () => { cancelled = true; };
}, [url]);
return (
<WrappedComponent
{...props}
data={data}
loading={loading}
error={error}
/>
);
};
};
}
// 使用
const UserListWithFetch = withFetch('/api/users')(UserList);
<UserListWithFetch />
7.2 withToggle #
jsx
function withToggle(WrappedComponent) {
return function WithToggleComponent(props) {
const [isOn, setIsOn] = useState(false);
const toggle = () => setIsOn(prev => !prev);
const setOn = () => setIsOn(true);
const setOff = () => setIsOn(false);
return (
<WrappedComponent
{...props}
isOn={isOn}
toggle={toggle}
setOn={setOn}
setOff={setOff}
/>
);
};
}
// 使用
function Dropdown({ isOn, toggle, children }) {
return (
<div>
<button onClick={toggle}>Toggle</button>
{isOn && <div class="dropdown">{children}</div>}
</div>
);
}
const DropdownWithToggle = withToggle(Dropdown);
7.3 withLocalStorage #
jsx
function withLocalStorage(key, initialValue) {
return function (WrappedComponent) {
return function WithLocalStorageComponent(props) {
const [value, setValue] = useState(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [value]);
return (
<WrappedComponent
{...props}
storedValue={value}
setStoredValue={setValue}
/>
);
};
};
}
// 使用
const TodoListWithStorage = withLocalStorage('todos', [])(TodoList);
八、HOC vs Hooks #
8.1 对比 #
| 方面 | HOC | Hooks |
|---|---|---|
| 复用方式 | 包装组件 | 函数调用 |
| 嵌套问题 | 可能很深 | 扁平 |
| 调试 | 较难 | 较易 |
| 类型推断 | 复杂 | 简单 |
8.2 推荐使用 Hooks #
jsx
// HOC 方式
function withWindowSize(WrappedComponent) {
return function WithWindowSizeComponent(props) {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return <WrappedComponent {...props} windowSize={size} />;
};
}
// Hooks 方式(推荐)
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
}
// 使用
function MyComponent() {
const windowSize = useWindowSize();
return <div>{windowSize.width} x {windowSize.height}</div>;
}
九、最佳实践 #
9.1 不要修改原组件 #
jsx
// ❌ 错误:修改原组件
function withHOC(WrappedComponent) {
WrappedComponent.someProperty = 'value'; // 不要这样做
return WrappedComponent;
}
// ✅ 正确:返回新组件
function withHOC(WrappedComponent) {
return function EnhancedComponent(props) {
return <WrappedComponent {...props} />;
};
}
9.2 传递所有 Props #
jsx
function withHOC(WrappedComponent) {
return function EnhancedComponent(props) {
// ✅ 传递所有 props
return <WrappedComponent {...props} />;
};
}
9.3 复制静态方法 #
jsx
function withHOC(WrappedComponent) {
const EnhancedComponent = (props) => (
<WrappedComponent {...props} />
);
// 复制静态方法
Object.keys(WrappedComponent).forEach(key => {
EnhancedComponent[key] = WrappedComponent[key];
});
return EnhancedComponent;
}
十、总结 #
| 要点 | 说明 |
|---|---|
| 定义 | 接收组件返回组件的函数 |
| 属性代理 | 操作 props |
| 渲染劫持 | 控制渲染 |
| 组合 | 使用 compose 工具 |
| 命名 | with 前缀,设置 displayName |
核心原则:
- 不要修改原组件
- 传递所有 props
- 复制静态方法
- 优先使用 Hooks
最后更新:2026-03-28