跳到主要内容

React高阶组件(TS类型推导)

· 阅读需 3 分钟
Jason Rong
前端糕手

React高阶组件

前言

  • HOC高阶组件本质上是一个函数,往往接收一个函数组件并返回一个新的组件,在这个新的组件中对传入的组件进行了一些额外的操作。(即对原来的的组件内容进行补充或者增强)

  • 在开发场景中,HOC高阶组件往往充当着工具组件的角色,他并不侵入性的影响原来的组件。基于这个原则,在HOC中去组织一些复用性的代码逻辑,从而控制其子树UI的渲染。

  • 与自定义Hook不同,Hook只能将我们的代码逻辑抽离到一个地方,但并不能直接影响UI的渲染,而是通过Hook提供的内容自己去控制视图内容。

HOC的作用(不考虑反向继承,有侵入性影响)

  1. 负责补充传递props
  2. 直接在HOC内部去控制原有组件的渲染(不改变原有组件的前提下)。

二者并非互斥的关系,HOC的核心就是由自己内部执行特定逻辑从而影响、控制原有组件的内容。

代码示例

目标示例: 哨兵进入视口时通知子组件(常见于无限滚动「快到底了」)。注意:isVisible 表示是否可见,不等于业务上的 isLoading(是否在请求中)。下面用 isVisible 注入,避免语义混淆。

  1. 补充 Props:
import { ComponentType, useEffect, useMemo, useRef, useState } from 'react';

type WithVisible = { isVisible?: boolean };

function ObserverHOC<P extends WithVisible>(Component: ComponentType<P>) {
const WrappedComponent = (props: Omit<P, 'isVisible'>) => {
const domRef = useRef<HTMLDivElement>(null);
const [isVisible, setIsVisible] = useState(false);
const observer = useMemo(
() =>
new IntersectionObserver(entries => {
entries.forEach(entry => {
setIsVisible(entry.isIntersecting);
});
}),
[]
);
useEffect(() => {
const dom = domRef.current;
if (dom) observer.observe(dom);
return () => {
observer.disconnect();
};
}, [observer]);
return (
<div ref={domRef}>
<Component {...(props as P)} isVisible={isVisible} />
</div>
);
};
WrappedComponent.displayName = `ObserverHOC(${Component.displayName || Component.name || 'Component'})`;
return WrappedComponent;
}

export default ObserverHOC;

Omit<P, 'isVisible'> 表示包装后的组件不必再手动传 isVisible,由 HOC 注入。子组件内部仍可把 isVisible 当成「触发下一页加载」的信号,另用本地 state 管真正的 isLoading

React-HOC-1.png

如果你漏传了Props,react也能提供相应的类型提示。

React-HOC-2.png

当然,这样需要你确定HOC本身传递的Props的类型和字段,保证其被包装的组件能够正确接收到。

  1. 控制原有组件的渲染(不改变原有组件的前提下):
function ObserverMountHOC<P extends object>(Component: ComponentType<P>) {
const WrappedComponent = (props: P) => {
const domRef = useRef<HTMLDivElement>(null);
const [isVisible, setIsVisible] = useState(false);
const observer = useMemo(() => new IntersectionObserver(entries => {
entries.forEach(entry => setIsVisible(entry.isIntersecting));
}), []);
useEffect(() => {
const dom = domRef.current;
if (dom) observer.observe(dom);
return () => observer.disconnect();
}, [observer]);
return <div ref={domRef}>{isVisible && <Component {...props} />}</div>;
};
WrappedComponent.displayName = `ObserverMountHOC(${Component.displayName || Component.name || 'Component'})`;
return WrappedComponent;
}

这个例子 HOC 不改动子组件内部,只在 isVisible 为真时挂载子树(懒展示)。

React-HOC-3.png

总结

  • ComponentType<P> 接住原组件 props,返回组件上用 Omit 去掉由 HOC 注入的字段
  • 清理 IntersectionObserver 时优先 disconnect()
  • 可见性 ≠ 加载中:命名分开,业务状态仍放在子组件或数据层

现代代码里横切逻辑也常用 Hook;HOC 仍适合「包一层 UI / 统一注入 props」的场景。