React 中的高阶组件
HOC高阶组件typescript,即高阶组件,是React中重用组件逻辑的中间方法。 HOC 本身不是 React API 的一部分。 它是一种基于React组合特性的设计模式。
描述
高级组件的名字就透露出一种中等的氛围。 其实这个概念应该源自JavaScript的高级函数。 React 文档中还给出了一个高阶函数。 高阶组件的定义是一个接收组件并返回新组件的函数。
A higher-order component is a function that takes a component and returns a new component.
复制
具体来说,高阶组件是一个参数为组件、返回值为新组件的函数。 组件将 props 转换为 UI,高阶组件将一个组件转换为另一个组件。 HOC在React的第三方库中非常常见,例如Redux的connect和Relay的createFragmentContainer。
// 高阶组件定义
const higherOrderComponent = (WrappedComponent) => {
return class EnhancedComponent extends React.Component {
// ...
render() {
return ;
}
};
}
// 普通组件定义
class WrappedComponent extends React.Component{
render(){
//....
}
}
// 返回被高阶组件包装过的增强组件
const EnhancedComponent = higherOrderComponent(WrappedComponent);
复制
这里需要注意的是,不要试图以任何形式改变HOC中的组件原型,而是使用组合的形式,通过将组件包装在容器组件中来实现功能。 通常,有两种方法来实现高阶组件:
财产代理
例如,我们可以为传入的组件减少存储中的一个 id 属性值,并且可以通过高级组件向该组件添加一个 props。 当然,我们也可以在 JSX 中对 WrappedComponent 组件中的 props 进行操作。 注意,并不是要操作传入的WrappedComponent类,我们不应该直接改变传入的组件,而是在组合过程中对其进行操作。
const HOC = (WrappedComponent, store) => {
return class EnhancedComponent extends React.Component {
render() {
const newProps = {
id: store.id
}
return ;
}
}
}
复制
我们还可以使用高阶组件将新组件的状态放入打包组件中。 例如,我们可以使用高阶分量将不受控分量转换为受控分量。
class WrappedComponent extends React.Component {
render() {
return ;
}
}
const HOC = (WrappedComponent) => {
return class EnhancedComponent extends React.Component {
constructor(props) {
super(props);
this.state = { name: "" };
}
render() {
const newProps = {
value: this.state.name,
onChange: e => this.setState({name: e.target.value}),
}
return ;
}
}
}
复制
或者说我们的目的就是将其与其他组件进行包装,以达到布局或者样式的目的。
const HOC = (WrappedComponent) => {
return class EnhancedComponent extends React.Component {
render() {
return (
);
}
}
}
复制
反向继承
反向继承意味着返回的组件继承了前一个组件。 在反向继承中,我们可以做很多操作,修改state、props甚至翻转Element Tree。 反向继承有一个重要的点,它不能保证解析出完整的子组件树,即解析出的元素树包含组件(函数类型或Class类型),并且无法操作该组件的子组件。 当我们使用反向继承来实现高层组件时,我们可以通过渲染绑架来控制渲染。 具体来说,我们可以有意识地控制WrappedComponent的渲染过程来控制渲染控制的结果。 例如,我们可以根据一些参数来决定是否渲染组件。
const HOC = (WrappedComponent) => {
return class EnhancedComponent extends WrappedComponent {
render() {
return this.props.isRender && super.render();
}
}
}
复制
我们甚至可以通过重绘来绑架原始组件的生命周期。
const HOC = (WrappedComponent) => {
return class EnhancedComponent extends WrappedComponent {
componentDidMount(){
// ...
}
render() {
return super.render();
}
}
}
复制
因为实际上是继承关系,所以我们可以读取组件的props和state。 如果有必要,我们甚至可以更改、修改和删除 props 和 state。 当然,前提是变革带来的风险需要自己控制。 某些情况下,我们可能需要传入一些高阶属性的参数,那么我们可以通过柯里化的方式传入参数,配合高阶组件完成类似于组件上闭包的操作。
const HOCFactoryFactory = (params) => {
// 此处操作params
return (WrappedComponent) => {
return class EnhancedComponent extends WrappedComponent {
render() {
return params.isRender && this.props.isRender && super.render();
}
}
}
}
复制
HOC 和 Mixin
Mixin 和 HOC 都可以用来解决与横切关注点相关的问题。 Mixin 是一种混入模式。 实际使用中,Mixin还是很强大的。 它可以促使我们在多个组件中共享同一个方法,但同时也会不断减少组件的新方法和属性。 组件本身除了能够感知,甚至需要做相关处理(比如命名冲突、状态维护等),一旦混入了更多的模块,整个组件就变得不可维护。 Mixin 可能会引入不可见的属性,比如在渲染组件中使用 Mixin 方法给组件带来不可见的 props 和 state,并且 Mixins 之间可能会存在依赖和耦合,不利于代码维护。 另外,不同Mixin中的方法可能会互相冲突。 之前React官方建议使用Mixin来解决与横切关注点相关的问题,但由于使用Mixin可能会带来更多麻烦,所以现在官方建议使用HOC。 高阶组件HOC属于函数式编程的函数式编程思想。 被包装的组件不会感知到高级组件的存在,并且高级组件返回的组件将在原始组件之上具有改进的功能。 基于此React官方推荐使用高阶组件。
注意不要改动原来的组件
不要尝试更改 HOC 中的组件原型,或以其他方式更改它。
function logProps(InputComponent) {
InputComponent.prototype.componentDidUpdate = function(prevProps) {
console.log("Current props: ", this.props);
console.log("Previous props: ", prevProps);
};
// 返回原始的 input 组件,其已经被修改。
return InputComponent;
}
// 每次调用 logProps 时,增强组件都会有 log 输出。
const EnhancedComponent = logProps(InputComponent);
复制
这样做会造成一些不良后果。 一是输入组件不能再像 HOC 提出之前那样使用。 更严重的是,如果你用另一个同样改变了 componentDidUpdate 的 HOC 来引发它,那么上面的 HOC 将同时,这个 HOC 也很难应用于没有生命周期的功能组件。 修改传入组件的 HOC 是一种很差的具体形式,调用者必须知道它们是如何实现的,以避免与其他 HOC 发生冲突。 HOC不应该改变传入的组件,而应该采用组合的形式,通过将组件包装在容器组件中来实现功能。
function logProps(WrappedComponent) {
return class extends React.Component {
componentDidUpdate(prevProps) {
console.log("Current props: ", this.props);
console.log("Previous props: ", prevProps);
}
render() {
// 将 input 组件包装在容器中,而不对其进行修改,Nice!
return ;
}
}
}
复制
过滤道具
HOC 向组件添加功能,并且不应该大幅改变合约本身。 HOC 返回的组件应该维护与原始组件类似的套接字。 HOC应该透明地传输与自身无关的props,并且大多数HOC应该包含类似于下面的渲染方法。
render() {
// 过滤掉额外的 props,且不要进行透传
const { extraProp, ...passThroughProps } = this.props;
// 将 props 注入到被包装的组件中。
// 通常为 state 的值或者实例方法。
const injectedProp = someStateOrInstanceMethod;
// 将 props 传递给被包装组件
return (
);
}
复制
最大化可组合性
并非所有 HOC 都是相同的,有时它只接受一个参数,即包装组件。
const NavbarWithRouter = withRouter(Navbar);
复制
HOC一般可以接收多个参数。 例如,在Relay中,HOC额外接收一个配置对象来指定组件的数据依赖关系。
const CommentWithRelay = Relay.createContainer(Comment, config);
复制
最常见的 HOC 签名如下,connect 是一个返回高阶组件的高阶函数。
// React Redux 的 `connect` 函数
const ConnectedComment = connect(commentSelector, commentActions)(CommentList);
// connect 是一个函数,它的返回值为另外一个函数。
const enhance = connect(commentListSelector, commentListActions);
// 返回值为 HOC,它会返回已经连接 Redux store 的组件
const ConnectedComment = enhance(CommentList);
复制
这种方法可能看起来令人困惑或不必要,但它有一个有用的属性,例如 connect 函数返回的单参数 HOC 具有签名 Component => Component,并且输出类型与输入类型相同的函数很容易编写一起。 相同的属性还允许 connect 和其他 HOC 充当装饰器。 另外,很多第三方库都提供了compose工具功能,包括lodash、Redux、Ramda等。
const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent))
// 你可以编写组合工具函数
// compose(f, g, h) 等同于 (...args) => f(g(h(...args)))
const enhance = compose(
// 这些都是单参数的 HOC
withRouter,
connect(commentSelector)
)
const EnhancedComponent = enhance(WrappedComponent)
复制
不要在 render 方法中使用 HOC
React 的 diff 算法使用组件标识来确定是否应该更新现有子树或丢弃它并安装新的子树。 如果 render 返回的组件与之前 render 中的组件相同 === ,React 会通过用新的子树解析子树来递归更新子树,如果不相等,则完全卸载之前的子树。 通常使用时不需要考虑这一点,但它对于 HOC 来说非常重要,因为这意味着您不应该在组件的 render 方法中将 HOC 应用于组件。
render() {
// 每次调用 render 函数都会创建一个新的 EnhancedComponent
// EnhancedComponent1 !== EnhancedComponent2
const EnhancedComponent = enhance(MyComponent);
// 这将导致子树每次渲染都会进行卸载,和重新挂载的操作!
return ;
}
复制
这不仅仅是一个性能问题,重新挂载一个组件会导致该组件及其所有子组件的状态丢失,并且如果 HOC 是在组件外部创建的,那么该组件只会被创建一次。 因此每次渲染时都会是相同的组件,一般来说,这与您预期的性能一致。 在极少数需要动态调用 HOC 的情况下,您可以在组件的生命周期方法或其构造函数中调用它。
一定要复制静态方法
有时在 React 组件上定义静态方法很有用,例如 Relay 容器公开静态方法 getFragment 来轻松组合 GraphQL 片段。 但是,当您将 HOC 应用于组件时,原始组件将被容器组件包装,这意味着新组件不具有原始组件的任何静态方法。
// 定义静态函数
WrappedComponent.staticMethod = function() {/*...*/}
// 现在使用 HOC
const EnhancedComponent = enhance(WrappedComponent);
// 增强组件没有 staticMethod
typeof EnhancedComponent.staticMethod === "undefined" // true
复制
要解决这个问题,可以在返回之前将此方法复制到容器组件中。
function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
// 必须准确知道应该拷贝哪些方法 :(
Enhance.staticMethod = WrappedComponent.staticMethod;
return Enhance;
}
复制
但要做到这一点,你需要知道应该复制哪些方法,你可以使用 hoist-non-react-statics 依赖来手动复制所有非 React 静态方法。
import hoistNonReactStatic from "hoist-non-react-statics";
function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
hoistNonReactStatic(Enhance, WrappedComponent);
return Enhance;
}
复制
除了导入组件之外,另一个可行的方案是额外导入这个静态方法。
// 使用这种方式代替...
MyComponent.someFunction = someFunction;
export default MyComponent;
// ...单独导出该方法...
export { someFunction };
// ...并在要使用的组件中,import 它们
import MyComponent, { someFunction } from "./MyComponent.js";
复制
参考文献不会被通过
虽然高阶组件的约定是将所有 props 传递给包装组件,但这不适用于 refs。 这是因为 ref 实际上并不是一个 prop高阶组件typescript,就像一个 key,它由 React 专门处理。 如果将 ref 添加到 HOC 的返回组件中,则 ref 引用指向容器组件而不是打包组件。 这个问题可以通过React.forwardRef API明确转发到内部组件。 。
function logProps(Component) {
class LogProps extends React.Component {
componentDidUpdate(prevProps) {
console.log('old props:', prevProps);
console.log('new props:', this.props);
}
render() {
const {forwardedRef, ...rest} = this.props;
// 将自定义的 prop 属性 “forwardedRef” 定义为 ref
return ;
}
}
// 注意 React.forwardRef 回调的第二个参数 “ref”。
// 我们可以将其作为常规 prop 属性传递给 LogProps,例如 “forwardedRef”
// 然后它就可以被挂载到被 LogProps 包裹的子组件上。
return React.forwardRef((props, ref) => {
return ;
});
}
复制
例子
React
class WrappedComponent extends React.Component {
render() {
return ;
}
}
const HOC = (WrappedComponent) => {
return class EnhancedComponent extends React.Component {
constructor(props) {
super(props);
this.state = { name: "" };
}
render() {
const newProps = {
value: this.state.name,
onChange: e => this.setState({name: e.target.value}),
}
return ;
}
}
}
const EnhancedComponent = HOC(WrappedComponent);
const HOC2 = (WrappedComponent) => {
return class EnhancedComponent extends WrappedComponent {
render() {
return this.props.isRender && super.render();
}
}
}
const EnhancedComponent2 = HOC2(WrappedComponent);
var vm = ReactDOM.render(
,
document.getElementById("root")
);
复制
每日问题
https://github.com/WindrunnerMax/EveryDay
复制
参考
https://juejin.cn/post/6844903477798256647
https://juejin.cn/post/6844904050236850184
https://zh-hans.reactjs.org/docs/higher-order-components.htm
复制