#说说对高阶组件的理解?应用场景?
#一、是什么
高阶组件(Higher-Order Component,简称 HOC)是 React 中复用组件逻辑的一种高级技巧。它本身不是 React API 的一部分,而是基于 React 组合特性衍生出的一种设计模式。
从定义上看,高阶组件是一个接受组件作为参数并返回新组件的函数:
function withSomething(WrappedComponent: React.ComponentType) {
return function EnhancedComponent(props: any) {
// 增强逻辑
return <WrappedComponent {...props} />;
};
}这与 JavaScript 中的高阶函数概念一致——函数接受函数作为参数或返回函数。
#二、实现模式
#属性代理(Props Proxy)
属性代理是最常见的 HOC 实现方式,通过包裹组件来操纵 props、抽象 state 或包装额外的 JSX。
interface WithLoadingProps {
loading: boolean;
}
function withLoading<P extends object>(
WrappedComponent: React.ComponentType<P>
) {
return function WithLoadingComponent(props: P & WithLoadingProps) {
const { loading, ...restProps } = props;
if (loading) {
return (
<div className="loading-container">
<div className="spinner" />
<p>加载中...</p>
</div>
);
}
return <WrappedComponent {...(restProps as P)} />;
};
}
function UserList({ users }: { users: User[] }) {
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}
const UserListWithLoading = withLoading(UserList);
// 使用
<UserListWithLoading loading={isLoading} users={users} />#注入额外 Props
function withUserInfo<P extends { user: User }>(
WrappedComponent: React.ComponentType<P>
) {
return function WithUserInfoComponent(props: Omit<P, 'user'>) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
fetchCurrentUser().then(setUser);
}, []);
if (!user) return <div>加载用户信息...</div>;
return <WrappedComponent {...(props as any)} user={user} />;
};
}
function Dashboard({ user }: { user: User }) {
return <h1>欢迎回来,{user.name}</h1>;
}
const DashboardWithUser = withUserInfo(Dashboard);
// 使用时不需要传 user
<DashboardWithUser />#反向继承(Inheritance Inversion)
反向继承通过继承被包裹组件来实现,可以访问组件的 state、生命周期和 render 方法。这种模式主要用于类组件,在函数组件时代较少使用。
function withRenderTracking(WrappedComponent: typeof React.Component) {
return class extends WrappedComponent {
render() {
console.log(`${WrappedComponent.name} 正在渲染`);
return super.render();
}
};
}由于反向继承与类组件深度耦合,且存在较多陷阱,现代 React 开发中不推荐使用。
#三、常见应用场景
#权限控制
interface AuthConfig {
requiredRole?: string;
redirectTo?: string;
}
function withAuth<P extends object>(
WrappedComponent: React.ComponentType<P>,
config: AuthConfig = {}
) {
const { requiredRole, redirectTo = '/login' } = config;
return function AuthenticatedComponent(props: P) {
const { user, isAuthenticated } = useAuth();
if (!isAuthenticated) {
return <Navigate to={redirectTo} replace />;
}
if (requiredRole && user?.role !== requiredRole) {
return (
<div className="forbidden">
<h2>403 - 权限不足</h2>
<p>您没有访问此页面的权限</p>
</div>
);
}
return <WrappedComponent {...props} />;
};
}
const AdminDashboard = withAuth(Dashboard, { requiredRole: 'admin' });
const UserProfile = withAuth(Profile);#日志记录与性能追踪
function withPerformanceTracking<P extends object>(
WrappedComponent: React.ComponentType<P>,
componentName?: string
) {
const displayName = componentName || WrappedComponent.displayName || WrappedComponent.name;
return function TrackedComponent(props: P) {
const renderStart = useRef(performance.now());
useEffect(() => {
const renderTime = performance.now() - renderStart.current;
console.log(`[性能] ${displayName} 渲染耗时: ${renderTime.toFixed(2)}ms`);
if (renderTime > 16) {
console.warn(`[性能警告] ${displayName} 渲染超过一帧 (${renderTime.toFixed(2)}ms)`);
}
});
return <WrappedComponent {...props} />;
};
}
const TrackedUserList = withPerformanceTracking(UserList, 'UserList');#主题注入
function withTheme<P extends { theme: Theme }>(
WrappedComponent: React.ComponentType<P>
) {
return function ThemedComponent(props: Omit<P, 'theme'>) {
const theme = useContext(ThemeContext);
return <WrappedComponent {...(props as any)} theme={theme} />;
};
}
function StyledButton({ theme, children, ...rest }: { theme: Theme; children: React.ReactNode }) {
return (
<button
style={{
backgroundColor: theme.primaryColor,
color: theme.textColor,
borderRadius: theme.borderRadius,
}}
{...rest}
>
{children}
</button>
);
}
const ThemedButton = withTheme(StyledButton);#数据获取抽象
function withDataFetching<P extends { data: any }, Q = {}>(
WrappedComponent: React.ComponentType<P>,
fetchFn: (params: Q) => Promise<any>
) {
return function DataFetchingComponent(props: Omit<P, 'data'> & Q) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetchFn(props as any as Q)
.then(result => {
if (!cancelled) {
setData(result);
setLoading(false);
}
})
.catch(err => {
if (!cancelled) {
setError(err);
setLoading(false);
}
});
return () => { cancelled = true; };
}, []);
if (loading) return <div>加载中...</div>;
if (error) return <div>加载失败:{error.message}</div>;
return <WrappedComponent {...(props as any)} data={data} />;
};
}
const UserListWithData = withDataFetching(UserList, () => fetch('/api/users').then(r => r.json()));#四、编写 HOC 的约定与注意事项
#1. 透传不相关的 Props
HOC 应该透传所有与自身无关的 props:
function withSomething<P extends object>(WrappedComponent: React.ComponentType<P>) {
return function Enhanced({ extraProp, ...props }: P & { extraProp: string }) {
// 只消费 extraProp,其余全部透传
return <WrappedComponent {...(props as P)} />;
};
}#2. 设置 displayName 方便调试
function withAuth<P extends object>(WrappedComponent: React.ComponentType<P>) {
function WithAuth(props: P) {
// ...
return <WrappedComponent {...props} />;
}
const wrappedName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
WithAuth.displayName = `withAuth(${wrappedName})`;
return WithAuth;
}#3. 不要在 render 中使用 HOC
function App() {
// ❌ 每次渲染都会创建新组件,导致整个子树卸载/重新挂载
const EnhancedComponent = withSomething(MyComponent);
return <EnhancedComponent />;
}
// ✅ 在组件外部使用
const EnhancedComponent = withSomething(MyComponent);
function App() {
return <EnhancedComponent />;
}#4. 复制静态方法
HOC 默认不会复制被包裹组件的静态方法,可以使用 hoist-non-react-statics 库:
import hoistNonReactStatics from 'hoist-non-react-statics';
function withSomething<P extends object>(WrappedComponent: React.ComponentType<P>) {
function Enhanced(props: P) {
return <WrappedComponent {...props} />;
}
hoistNonReactStatics(Enhanced, WrappedComponent);
return Enhanced;
}#5. 转发 Ref
HOC 默认不会转发 ref,需要使用 React.forwardRef:
function withLogging<P extends object>(WrappedComponent: React.ComponentType<P>) {
const WithLogging = forwardRef<any, P>((props, ref) => {
useEffect(() => {
console.log('组件已挂载');
}, []);
return <WrappedComponent {...props} ref={ref} />;
});
WithLogging.displayName = `withLogging(${WrappedComponent.displayName || WrappedComponent.name})`;
return WithLogging;
}#五、HOC 的替代方案
#自定义 Hooks(推荐)
自定义 Hooks 是目前 React 中最推荐的逻辑复用方式,相比 HOC 更加灵活和直观:
// HOC 方式
const EnhancedComponent = withAuth(withTheme(withLogging(MyComponent)));
// Hooks 方式
function MyComponent() {
const { user, isAuthenticated } = useAuth();
const theme = useTheme();
useLogging('MyComponent');
if (!isAuthenticated) return <Navigate to="/login" />;
return <div style={{ color: theme.primaryColor }}>...</div>;
}#Render Props
Render Props 是另一种逻辑复用模式,通过函数 prop 来共享逻辑:
function MouseTracker({ render }: { render: (pos: { x: number; y: number }) => JSX.Element }) {
const [pos, setPos] = useState({ x: 0, y: 0 });
useEffect(() => {
const handler = (e: MouseEvent) => setPos({ x: e.clientX, y: e.clientY });
window.addEventListener('mousemove', handler);
return () => window.removeEventListener('mousemove', handler);
}, []);
return render(pos);
}
// 使用
<MouseTracker render={({ x, y }) => <p>鼠标位置:{x}, {y}</p>} />#三种方式对比
| 维度 | HOC | Render Props | 自定义 Hooks |
|---|---|---|---|
| 嵌套问题 | 多层嵌套 | 回调嵌套 | 扁平调用 |
| TypeScript 支持 | 类型推导复杂 | 较好 | 最佳 |
| 调试体验 | 组件层级深 | 组件层级深 | 无额外层级 |
| 灵活性 | 静态组合 | 动态组合 | 最灵活 |
| 学习成本 | 中等 | 中等 | 低 |
| 适用场景 | 第三方库封装 | 动态渲染逻辑 | 通用逻辑复用 |
#六、总结
高阶组件是 React 中一种成熟的设计模式,在权限控制、日志记录、数据注入等场景仍然有其价值。但在现代 React 开发中,自定义 Hooks 因其更好的组合性、类型安全性和调试体验,已经成为逻辑复用的首选方案。理解 HOC 的原理和限制,有助于维护老项目代码,也有助于理解许多第三方库(如 React Router 的 withRouter、Redux 的 connect)的设计思路。