高阶组件

168 阅读9分钟

高阶组件

高阶组件其实就是处理react组件的函数 (组件是将数据转换为 UI,而高阶组件是将组件转换为另一个组件)

高阶组件本质是一个函数,它接受一个组件作为参数,并返回一个新的组件。高阶组件并不改变传入的组件本身。定义一个函数,传入一个组件,然后在定义的函数内部再实现一个函数对传入的组件去扩展一些功能,最后返回一个新的组件,这就是高阶组件的概念,作用就是为了更好的复用代码。

高阶组件可以实现一些功能,比如:

  • 数据注入:高阶组件可以通过props将一些数据注入到组件中,使组件能够访问这些数据
  • 条件渲染:高阶组件可以根据一些条件来控制组件是否渲染
  • 包裹组件:高阶组件可以将组件包裹在一个容器组件中,提供额外的功能和样式
  • 对组件进行修饰:高阶组件可以通过修改组件的props或state来修饰组件的行为或样式
// higherOrderComponent就是高阶组件(其实它是一个函数),负责处理组件公共逻辑
// WrappedComponent组件是要使用高阶组件的组件,负责该组件自身的业务逻辑和UI展示
// EnhancedComponent就是注入了公共逻辑的新组件
const EnhancedComponent = higherOrderComponent(WrappedComponent);

image.png

高阶组件的基本用法

创建一个简单的高阶组件:

// TestHoc.js
import React from "react"
// 调用该方法会生成一个组件,使用生成的组件时所接受的props属性需要一并传给使用高阶组件的那个组件
export default function TestHoc(UserCom) {
    // component需要传入一个组件,传入的组件就是需要使用高阶组件的那个组件
    // 需要复用的逻辑写在返回的新的组件中,新组件可以是类组件也可以是函数组件(新组件可以不用命名,是匿名组件)
    // 这里返回一个类组件,需要复用的逻辑就是传递一个参数
    return class extends React.Component {
        state = {
            a: 123
        }
        look = () => {
            console.log(this.state.a);
        }
        change = (val) => {
            console.log(this, 99)
            this.setState({ a: val })
        }
        // 新组件返回传入的需要复用逻辑的组件
        render() {
            return (
                <>
                    {/* 传给高阶组件的props属性需要一并传给使用高阶组件的那个组件 ,...this.props它就是使用高阶组件的那个组件的属性*/}
                    {/* 通过props将一些数据注入到组件中 */}
                    <UserCom a={this.state.a} look={this.look} change={this.change} {...this.props} />
                </>
            )
        }
    }

}
// App.jsx
import React from "react"
import TestHoc from "./TestHoc.js"; // 引入高阶组件
import Son from "./Son.jsx"; // 引入使用高阶组件的组件
// TestHoc并不是一个组件,而是一个方法,需要调用它并传入一个需要使用高阶组件的组件
let HocSon = TestHoc(Son);
export default function App() {
  return (
    <div>
      <h1>App组件</h1>
      {/* 使用高阶组件所返回的新组件 */}
      <HocSon />
    </div>
  )
}
// Son.jsx
import React from "react";
const Son = (props) => {
    console.log(props, "高阶组件传入的数据");
    const changeVal = (val) => {
        props.change(val)
    }
    const lookVal = () => {
        console.log(props.a)
    }
    return (
        <>
            <h2>Son子组件</h2>
            <p>高阶组件的使用其数据为:{props.a}</p>
            <button onClick={() => { changeVal(456) }}>修改数据</button>
            <button onClick={lookVal}>查看数据</button>
        </>
    )
}
export default Son;

2.gif

使用高阶组件的步骤如下:

  1. 创建一个高阶组件函数,接收一个组件作为参数并返回一个新的组件(返回的新组件可以是类组件也可以是函数组件 )
  2. 在高阶组件函数中,通过props将一些数据注入到组件中
  3. 使用高阶组件时,将组件作为参数传递给高阶组件函数即可,返回的新组件会包含高阶组件提供的功能。

高阶组件使用举例

案例一:根据条件来渲染内容

// testHoc.js
import React, { useState, useEffect } from 'react';
// HOC 函数
export default function withLoading(WrappedComponent) {
    return function EnhancedComponent(props) {
        // 使用 useState 定义 isLoading 状态变量,初始值为 true,表示组件正在加载
        const [isLoading, setIsLoading] = useState(true);
        // useEffect 钩子用于执行副作用操作,这里用于模拟数据加载过程
        useEffect(() => {
            // 模拟数据加载过程
            const timer = setTimeout(() => {
                setIsLoading(false);
            }, 2000);

            // 清理定时器
            return () => clearTimeout(timer);
        }, []);
        // 条件渲染:isLoading 为 true,则返回一个显示 "Loading..." 的 div 元素,isLoading 为 false,则渲染 WrappedComponent 并传递所有的 props。
        if (isLoading) {
            return <div>Loading...</div>;
        }

        return <WrappedComponent {...props} />;
    };
}
// App.jsx
import React from "react"
import withLoading from "./testHoc.js"; // 引入高阶组件
function App() {
  return (
    <div>
      <h1>App组件</h1>
      <div>这里是我的组件内容</div>
    </div>
  )
}
// 应用 HOC
const EnhancedComponent = withLoading(App);
export default EnhancedComponent;

withLoading 是一个高阶组件函数,它接受一个组件 WrappedComponent 作为参数。withLoading负责处理数据是否加载完并展示数据,而MyComponent组件,它只负责渲染内容

2.gif

案例二:封装获取鼠标位置的逻辑

// testHoc.js
import React, { useEffect, useState } from "react";
// 定义一个高阶组件
export default function getCurrsor(UseComponent) {
    return (props) => {
        let [clientInfo, setClientInfo] = useState({
            x: 0,
            y: 0
        })
        // 获取鼠标位置
        useEffect(() => {
            window.addEventListener("mousemove", (e) => {
                setClientInfo({
                    x: e.clientX,
                    y: e.clientY
                })
            })
        })
        return (
            <>
                <UseComponent clientInfo={clientInfo} {...props} />
            </>
        )
    }
}
// App.jsx
import React from "react"
import testHoc from "./testHoc.js"; // 引入高阶组件
import Son from "./Son.jsx"; // 引入使用高阶组件的组件
// testHoc并不是一个组件,而是一个函数,需要调用它并传入一个需要使用高阶组件的组件
const HocSon = testHoc(Son)
export default function App() {
  return (
    <div>
      <h1>App组件</h1>
      {/* 使用高阶组件所返回的新组件 */}
      <HocSon></HocSon>
    </div>
  )
}

// Son.jsx
import React from "react";
const Son = (props) => {
    console.log(props,"props")
    return (
        <>
            <h2>Son组件</h2>
            <p>x的位置:{props.clientInfo.x}</p>
            <p>y的位置:{props.clientInfo.y}</p>
        </>
    )
}
export default Son;

2.gif

高阶组件实现方式

高阶组件有两种实现方式:

  1. 属性代理
  2. 反向继承

属性代理

属性代理是最常见的高阶组件的实现方式。这种方式通过将额外的props传递给被包装的组件来实现功能增强。

属性代理实现步骤

  1. 创建一个新的函数:该函数接收一个组件作为参数。
  2. 定义新的组件:在函数内部定义一个新的组件,该组件可以拥有自己的状态和生命周期方法。
  3. 传递额外的props:新的组件在渲染时会将额外的props传递给被包装的组件。
  4. 返回新的组件:返回这个新的组件。

属性代理示例

// testHoc.js
import React, { Component } from 'react';
// HOC 函数
export default function withLoading(WrappedComponent) {
    return class extends Component {
        state = {
            isLoading: true,
        };

        componentDidMount() {
            // 模拟数据加载过程
            setTimeout(() => {
                this.setState({ isLoading: false });
            }, 2000);
        }

        render() {
            const { isLoading } = this.state;
            if (isLoading) {
                return <div>Loading...</div>;
            }
            return <WrappedComponent {...this.props} />;
        }
    };
}
// App.jsx
import React from "react"
import withLoading from "./testHoc.js"; // 引入高阶组件
function App() {
  return (
    <div>
      <h1>App组件</h1>
      <div>这里是我的组件内容</div>
    </div>
  )
}
// 应用 HOC
const EnhancedComponent = withLoading(App);
export default EnhancedComponent;

2.gif

反向继承

反向继承是一种不太常见的高阶组件的实现方式,但它在某些情况下非常有用。这种方式通过继承被包装的组件来实现功能增强。

反向继承实现步骤

  1. 创建一个新的函数:该函数接收一个组件作为参数。
  2. 定义新的组件:在函数内部定义一个新的组件,该组件继承自被包装的组件。
  3. 扩展子类的方法:可以在新的组件中重写或扩展被包装组件的方法。
  4. 返回新的组件:返回这个新的组件。

反向继承示例

// testHoc.js
import React from 'react';
// HOC 函数
export default function withLoading(WrappedComponent) {
    // 返回一个新的类,这个类继承自 WrappedComponent,这意味着新的类会继承 WrappedComponent 的所有属性和方法。
    return class extends WrappedComponent {
        constructor(props) {
            // 构造函数中调用 super(props) 来初始化父类WrappedComponent的构造函数
            super(props);
            this.state = {
                isLoading: true,
            };
        }

        componentDidMount() {
            // 模拟数据加载过程
            setTimeout(() => {
                this.setState({ isLoading: false });
            }, 2000);
        }

        render() {
            const { isLoading } = this.state;
            if (isLoading) {
                return <div>Loading...</div>;
            }
            // isLoading 为 false,调用 super.render() 渲染 WrappedComponent
            return super.render();
        }
    };
}
// App.jsx
import React from "react"
import withLoading from "./testHoc.js"; // 引入高阶组件
class App extends React.Component {
  render() {
    return <div>这里是我的组件内容</div>;
  }
}
// 应用 HOC
const EnhancedComponent = withLoading(App);
export default EnhancedComponent;

2.gif

这个示例实现了:

  • 继承:新的组件类继承自 WrappedComponent,这样可以访问和修改 WrappedComponent 的内部方法和状态。
  • 状态管理:通过 isLoading 状态来控制是否显示加载提示。
  • 生命周期方法:在 componentDidMount 中模拟数据加载过程,并在加载完成后更新状态。
  • 条件渲染:根据 isLoading 的值决定渲染加载提示还是 WrappedComponent

两种方式对比

属性代理:

  • 优点:简单直观,易于理解和使用。
  • 缺点:可能会导致 prop 冲突,尤其是在被包装组件已经定义了相同的 prop 时。
  • 适用场景:大多数情况下,特别是当需要传递额外 props 或状态时。

反向继承:

  • 优点:可以更直接地访问和修改被包装组件的内部方法和状态。
  • 缺点:耦合度更高,被包装组件的内部结构变化可能会影响 HOC 的工作。
  • 适用场景:需要深度修改被包装组件的行为,或者需要访问被包装组件的内部方法和状态。

函数组件本身不能继承,所以函数组件不能直接进行反向继承

总结

组件是既包含了UI界面也包含了逻辑,所以组件的复用包含了UI界面和逻辑的复用。

高阶组件只是复用组件逻辑,包括可以复用状态管理、生命周期方法等逻辑。高阶组件可以保持原始组件纯净,即原始组件只需关注UI渲染,其他逻辑可以抽离到高阶组件中。高阶组件可以增强组件功能,通过包裹现有组件,为其添加额外的功能或属性。

类似于vuemixin的用途,当某个操作逻辑,或者某个运算经常出现的时候,就可以提取为高阶组件。

vite 创建React项目使用高阶组件注意事项

vite不会对 js 做 jsx 的语法转换,就会出现报错:[plugin:vite:import-analysis] Failed to parse source for import analysis because the content contains invalid JS syntax. If you are using JSX, make sure to name the file with the .jsx or .tsx extension.

image.png

解决方案

安装 @babel/plugin-transform-react-jsx 的插件

npm i @babel/plugin-transform-react-jsx

然后配置vite.config.js

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vite.dev/config/
export default defineConfig({
  plugins: [react({
    babel: {
      plugins: ['@babel/plugin-transform-react-jsx'],
    },
  })],
  server: {
      host: "0.0.0.0"
      }
})

当出现Uncaught ReferenceError: React is not defined报错时,只需要在提示错误的文件中引入React即可。

import React,{ useState } from 'react'

image.png