React中的高阶组件(三)

218 阅读14分钟

接着介绍React中的高阶组件,上篇文章React中的高阶组件(二)

高阶组件(Higher-Order Component, HOC) 是React中为了复用组件的函数。一个 HOC 是一个函数,接收一个组件并返回一个增强版的组件。通过 HOC,开发者可以将通用逻辑提取到可复用的组件包装器中,避免代码重复。

9. withReduxForm

withReduxForm 是一个假设的高阶组件,用于集成 Redux 表单。通常,在 Redux 中处理表单状态时,借助 Redux Form 或类似的库,这样可以更好地管理表单数据和验证。

这段代码使用了 redux-form 库,它帮助在 React 应用中轻松处理表单状态。代码展示了如何创建一个表单组件,并使用 reduxForm 高阶组件(HOC)来将表单数据与 Redux 状态管理相结合。

import { reduxForm, Field } from 'redux-form';

const MyFormComponent = (props) => {
    const { handleSubmit } = props;
    return (
        <form onSubmit={handleSubmit}>
            <Field name="username" component="input" type="text" />
            <button type="submit">Submit</button>
        </form>
    );
};

export default reduxForm({
    form: 'myForm',
})(MyFormComponent);

主要技术点:

  1. reduxForm 高阶组件(HOC)reduxFormredux-form 库提供的一个 HOC,它将 React 表单组件与 Redux 的状态管理绑定在一起。

    • 它将表单的数据存储到 Redux 状态中,允许我们追踪、验证表单的输入和处理表单提交。
    • reduxForm 会为组件注入许多道具(例如 handleSubmit),以方便管理表单的行为。

    使用方法

export default reduxForm({
    form: 'myForm', // 定义表单的名称,在 Redux 状态树中以 `form.myForm` 的形式存储
})(MyFormComponent);

form: 'myForm' 指定了表单的名称,这个表单的状态会被存储在 Redux 中的 form.myForm 下。

  1. Field 组件Fieldredux-form 中的一个组件,用于渲染表单中的输入控件。它的作用是:
  • 管理表单中的每个字段的状态。
  • Field 组件需要传入一个 name 属性,用来标识输入字段的名称,同时 component 属性指定这个 Field 渲染的实际表单控件类型(例如 input, select, textarea 等)。

示例

<Field name="username" component="input" type="text" />
  • name="username":该字段会存储在表单的 username 字段中。

  • component="input":渲染为一个 HTML 的 input 元素。

  • type="text":指定 input 元素的类型为 text

  1. handleSubmit 函数
  • reduxForm 会自动将 handleSubmit 作为道具传递给组件。handleSubmit 函数的作用是:

    • 阻止表单的默认提交行为。
    • 验证表单中的所有字段。
    • 调用一个自定义的提交函数,通常是通过组件道具传入的。

通过 handleSubmit 传入自定义的表单处理逻辑。例如:

const onSubmit = (formValues) => {
    console.log(formValues);
};

<MyFormComponent onSubmit={onSubmit} />

redux-form 如何工作?

  1. 表单状态管理reduxForm 将表单的值、有效性、提交状态等所有信息都存储在 Redux 的 form 分支下。每个表单字段的状态都会自动同步到 Redux 中,方便集中管理表单数据。

  2. 数据流

    • 当用户输入时,Field 组件会捕获输入值,并将其存储到 Redux 状态中。
    • 提交表单时,reduxForm 会将整个表单的值传递给 handleSubmit

简单应用:

  1. 表单创建: 使用 reduxForm 包装的组件会在 Redux 中自动管理表单状态,而不需要我们手动处理。
  2. 表单验证: 可以通过 reduxForm 提供的机制轻松进行字段级或表单级验证。

完整示例:

import React from 'react';
import { reduxForm, Field } from 'redux-form';

const MyFormComponent = (props) => {
    const { handleSubmit } = props;

    // 表单提交处理
    const onSubmit = (formValues) => {
        
        // 验证 username 是否为 'aab' 
        if (formValues.username === 'aab') { 
            console.log('Submitted Values:', formValues); 
        } else { 
            alert('Username must be "aab"'); 
        }
        
    };

    return (
        <form onSubmit={handleSubmit(onSubmit)}>
            <div>
                <label htmlFor="username">Username:</label>
                <Field name="username" component="input" type="text" />
            </div>
            <div>
                <label htmlFor="password">Password:</label>
                <Field name="password" component="input" type="password" />
            </div>
            <button type="submit">Submit</button>
        </form>
    );
};
// 将表单连接到 redux-form
export default reduxForm({
    form: 'myForm',
})(MyFormComponent);

解释

  1. onSubmit 验证逻辑

    • onSubmit 函数中,我们从 formValues 中获取 username 字段的值,并检查是否等于 "aab"。
    • 如果等于 "aab",表单可以提交,执行相应的提交逻辑;否则,会弹出一个提示框告知用户 "Username must be 'aab'"。
  2. reduxForm 高阶组件

    • reduxFormredux-form 的高阶组件(HOC),它将表单的状态连接到 Redux。
    • reduxForm 提供了 handleSubmit 函数,该函数会处理表单验证、阻止默认提交行为,并将验证通过的表单数据传递给 onSubmit 函数。

如果删除 reduxForm 以下包装代码

export default reduxForm({
    form: 'myForm',
})(MyFormComponent);

如果删除这部分代码,表单将无法与 redux-form 的机制进行交互,以下功能将失效:

  • 表单的状态将不再被存储在 Redux 中。
  • handleSubmit 将不再可用,因此表单的验证和提交处理将无法正常工作。
  • 表单的字段状态(如 usernamepassword)将不再由 redux-form 组件自动管理。

在这种情况下,你需要手动处理表单的验证和状态管理,但这会导致复杂性增加,失去 redux-form 带来的自动化优势。

总结

  • redux-form 通过将表单与 Redux 绑定,让表单状态管理变得更加可预测和一致。
  • 使用 reduxForm HOC 来包装表单组件,使得表单状态能够与 Redux 中的全局状态结合。
  • 通过在 onSubmit 函数中进行自定义验证可以实现对输入值的条件判断。
  • Field 组件负责渲染表单字段并自动处理状态同步。

10. withCache

withCache 是一个假设的高阶组件,用于将缓存功能注入到 React 组件中,通常用于减少重复的 API 请求或缓存昂贵的计算结果。

这段代码定义了一个名为 withCache 的高阶组件(HOC),它用于缓存从外部来源获取的数据,并将缓存的数据作为 props 传递给被包装的组件 WrappedComponent。这样做可以避免重复获取数据,提升性能,特别是在需要多次渲染相同数据的场景中。

const withCache = (WrappedComponent, key, fetchData) => {
    return class extends React.Component {
        constructor(props) {
            super(props);
            this.state = {
                data: Cache.get(key) || null,
            };
        }

        async componentDidMount() {
            if (!this.state.data) {
                const data = await fetchData();
                Cache.set(key, data);
                this.setState({ data });
            }
        }

        render() {
            return <WrappedComponent data={this.state.data} {...this.props} />;
        }
    };
};

解释:

  • withCache 是用于缓存数据的高阶组件。它会检查缓存中是否有对应的数据(通过 key),如果有则直接使用缓存;如果没有,则通过 fetchData 获取数据并将其存储在缓存中。
  • 这种模式可以减少不必要的 API 请求或重复计算,从而提升应用的性能。

代码结构和工作原理

  1. withCache 函数的参数

    • WrappedComponent: 需要被缓存数据的 React 组件。
    • key: 用于标识缓存数据的键值(例如,可以使用与数据相关的唯一标识符)。
    • fetchData: 获取数据的异步函数,该函数在缓存中不存在数据时执行。
  2. constructor 和初始状态

    • constructor 中,组件的初始状态设置为从缓存中获取的数据。如果缓存中已经有了数据,则直接将其赋值给 data,否则初始值为 null
    • Cache.get(key) 用于从缓存中读取数据。如果缓存中没有与该键对应的数据,则返回 null
  3. componentDidMount 生命周期方法

    • 当组件挂载时,componentDidMount 会被调用。
    • 如果组件的状态中 datanull(即缓存中没有数据),则执行 fetchData 函数来异步获取数据。
    • 获取到数据后,将其存入缓存 Cache.set(key, data),同时更新组件的状态,以触发重新渲染并将数据传递给被包装的组件。
  4. render 方法

    • 渲染时,WrappedComponent 会接收从缓存中获取的数据(或从 fetchData 异步获取的数据)作为 data 属性传递下去。
    • ...this.props 确保组件保留了原始的 props,并将其传递给被包装的组件。

使用场景

withCache 高阶组件适用于那些需要频繁访问相同数据的组件,尤其是数据的获取过程涉及较高的延迟或消耗大量资源(如 API 请求、数据库查询等)。通过缓存机制,能够避免重复请求,减少不必要的性能开销。

代码示例:如何使用 withCache

// 假设这是一个简单的缓存实现
const Cache = {
    store: {},
    get(key) {
        return this.store[key];
    },
    set(key, value) {
        this.store[key] = value;
    },
};

// 异步获取数据的函数
const fetchData = async () => {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    return data;
};

// 原始组件,显示从缓存中获取的数据
const MyComponent = ({ data }) => {
    if (!data) {
        return <div>Loading...</div>;
    }
    return <div>Data: {data}</div>;
};

// 使用 withCache 高阶组件对 MyComponent 进行包装
const CachedMyComponent = withCache(MyComponent, 'myDataKey', fetchData);

// 在应用中使用
const App = () => (
    <div>
        <CachedMyComponent />
    </div>
);

export default App;

解释

  1. 缓存系统

    • Cache.get(key)Cache.set(key, value) 是简单的缓存存储实现。实际应用中,缓存机制可以使用浏览器的 localStoragesessionStorage,或其他复杂的缓存系统。
  2. 异步获取数据

    • fetchData 是一个用于异步获取数据的函数。在这个例子中,它发起了一个 HTTP 请求,获取数据并返回结果。
  3. 被包装的组件

    • MyComponent 接收 data 作为属性,并渲染数据。如果数据尚未加载,它会显示 "Loading..."。
  4. 高阶组件包装

    • CachedMyComponent 是使用 withCache 包装后的组件,它负责处理数据的缓存逻辑。这个组件会检查缓存中是否已有数据,如果有则直接使用缓存,否则发起异步请求获取数据并缓存。
  5. 关于构造函数

在 JavaScript 的 class 组件中,super(props) 是调用父类(在这种情况下是 React.Component)的构造函数并将 props 传递给父类的一个必要步骤。

具体含义

  1. constructor:

    • constructor 是类的构造函数,当使用 new 关键字创建类的实例时,构造函数会被调用。对于 React 组件,constructor 通常用于初始化组件的状态(state)或绑定事件处理程序。
  2. super(props) :

    • super() 是用来调用父类的构造函数。在 ES6 类继承中,如果一个类继承了另一个类(例如 class MyComponent extends React.Component),那么在派生类的构造函数中必须首先调用 super(),才能访问 this 关键词。
    • props 是组件的属性对象,super(props) 将这些属性传递给父类(React.Component),以便父类可以正确初始化该组件。React 中的 Component 基类会在内部处理 props,并将其作为组件的一个默认属性对象进行管理。

为什么必须调用 super(props)

在 React 中,当一个组件继承自 React.Component 时,必须在构造函数中调用 super(),因为这是从父类中继承并初始化该组件的一部分逻辑。如果不调用 super(),则无法正确初始化组件,甚至会导致 JavaScript 抛出错误(不能访问 this)。

此外,props 是由父组件传递给当前组件的,因此通过调用 super(props),这些 props 可以被 React.Component 正确处理和初始化,并使得我们可以在 constructor 中访问 this.props

实例解释

以下是一个简单的例子来说明 super(props) 的作用:

class MyComponent extends React.Component {
    constructor(props) {
        super(props); // 需要调用 super(props),以便在父类中正确初始化 props
        this.state = {
            data: Cache.get(this.props.key) || null, // 现在 this.props 可以被访问
        };
    }

    render() {
        return <div>Data: {this.state.data}</div>;
    }
}

在这个例子中:

  • super(props) 确保了 this.props 被正确初始化。
  • 如果不调用 super(props),你将无法访问 this.props,例如在构造函数中访问 this.props.key 会导致错误。

11. withErrorBoundary(错误边界)

  • 功能:捕获组件中的 JavaScript 错误并展示错误信息。
  • 示例: 这段代码是一个 React 错误边界(Error Boundary)组件,名为 ErrorBoundary。它的作用是捕获其子组件树中发生的 JavaScript 错误,并显示一个降级的 UI,而不是直接崩溃整个应用。错误边界是一种可靠的错误处理机制,常用于大型 React 应用中。
class ErrorBoundary extends React.Component {
    constructor(props) {
        super(props);
        // 初始化组件状态,表示是否捕获了错误
        this.state = { hasError: false };
    }

    // 静态方法,用于从捕获到的错误更新 state
    static getDerivedStateFromError(error) {
        // 更新 state 以显示备用的 UI
        return { hasError: true };
    }

    render() {
        // 如果 state 表示有错误,渲染错误提示
        if (this.state.hasError) {
            return <div>Something went wrong.</div>;
        }

        // 否则渲染正常的子组件
        return this.props.children;
    }
}

export default ErrorBoundary;

关键部分的详细解释

  1. constructor(props) :

    • 在构造函数中,组件的初始状态被定义为 { hasError: false },表示初始时没有捕获任何错误。
    • 这个状态值会在组件检测到错误时被更新。
  2. static getDerivedStateFromError(error) :

    • 这是 React 中的一个特殊静态生命周期方法。它在子组件树中的任何组件抛出错误时被调用。
    • 该方法会从错误中更新状态,并将 hasError 设置为 true,以告知组件捕获到了错误。
    • 使用这个方法,ErrorBoundary 可以安全地更新状态并让 render 方法渲染错误提示界面。
  3. render() :

    • 如果 state.hasErrortrue,则渲染一个简单的降级 UI 提示用户 "Something went wrong."。
    • 否则,它会正常渲染子组件树中的内容,this.props.children 代表 ErrorBoundary 包裹的所有子组件。

用法

ErrorBoundary 通常用于包裹子组件树,以捕获在这些子组件中的任何错误。典型用法如下:

import React from 'react';
import ErrorBoundary from './ErrorBoundary'; // 假设这是上面的ErrorBoundary

const MyComponent = () => {
    // 假设这里可能发生一个错误
    throw new Error('Test error');
    return <div>Hello World</div>;
};

const App = () => (
    <ErrorBoundary>
        <MyComponent />
    </ErrorBoundary>
);

export default App;

这个代码运行后,不会显示 "Test error",而是会显示 "Something went wrong."

具体过程:

  1. MyComponent 抛出错误时,React 会调用 ErrorBoundary 组件的静态方法 getDerivedStateFromError
  2. 该方法会将 ErrorBoundary 的状态更新为 { hasError: true }
  3. 随后,在 render 方法中,由于 this.state.hasErrortrueErrorBoundary 不会渲染其 children(即 MyComponent),而是渲染 "Something went wrong."

这是因为 ErrorBoundary 组件的作用是捕获子组件树中的错误。在 MyComponent 中抛出的错误(throw new Error('Test error'))会被 ErrorBoundary 组件捕获。然后,ErrorBoundary 会将其内部状态 hasError 设置为 true,并在 render 方法中渲染降级 UI "Something went wrong."

注意事项

  • 错误边界只能捕获渲染周期、生命周期方法构造函数中的错误。

  • 它无法捕获以下类型的错误:

    1. 事件处理函数中的错误。
    2. 异步代码中的错误(如 setTimeoutrequestAnimationFrame 回调函数)。
    3. 服务端渲染中的错误。
    4. 自身组件(即错误边界组件本身)内部的错误。

扩展

要捕获更多类型的错误,可以在事件处理函数或异步代码中显式使用 try-catch,例如:

try {
    // 可能出错的异步代码
} catch (error) {
    // 错误处理逻辑
}

12. withLogging(为组件添加日志记录)

  • 功能withLogging 是一个高阶组件(Higher-Order Component, HOC),用于给某个 React 组件添加日志记录功能。在项目中,我们可以使用 withLogging 来增强现有的组件,使其能够在生命周期方法(加载时、更新时、卸载时)、用户交互或其他事件中记录日志。
  • 示例: 创建withLogging组件
import React from "react";

const withLogging = (WrappedComponent) => {
  return class extends React.Component {
    componentDidMount() {
      console.log(`Component ${WrappedComponent.name} mounted`);
    }

    componentDidUpdate(prevProps) {
      console.log(`Component ${WrappedComponent.name} updated`);
      console.log('Previous props:', prevProps);
      console.log('Current props:', this.props);
    }

    componentWillUnmount() {
      console.log(`Component ${WrappedComponent.name} will unmount`);
    }

    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
};

export default withLogging;

应用withLogging函数组件到App.js

import React, { Suspense } from "react";
import { useSelector } from "react-redux";
import { withLogging } from "./hoc/withLogging"; // 假设 withLogging 位于 hoc 文件夹中

const App = () => {
  const themeConfig = useSelector((state) => state.siteConfig.theme);

  return (
    <React.Fragment>
      <div>
        <h1>Welcome to Cloudreve</h1>
      </div>
    </React.Fragment>
  );
};

export default withLogging(App);

在这个例子中,App 组件被 withLogging 包装。当 App 组件挂载、更新或卸载时,控制台会记录日志。例如,当组件挂载时,控制台会输出:

Component App mounted
  • 示例2: 应用withLogging类组件到Navbar.js
import React, { Component } from "react";
import { connect } from "react-redux";
import { withRouter } from "react-router-dom";
import { withLogging } from "../hoc/withLogging"; // 假设 withLogging 位于 hoc 文件夹中

class NavbarComponent extends Component {
  render() {
    return (
      <nav>
        <h2>Navbar</h2>
      </nav>
    );
  }
}

const mapStateToProps = (state) => ({
  user: state.user,
});

export default connect(mapStateToProps)(withRouter(withLogging(NavbarComponent)));

在这个例子中,NavbarComponent 组件也被 withLogging 包装。无论是初次加载、更新,还是页面导航时,该组件的生命周期都会记录日志。

withLogging 在实际项目中的作用

withLogging 的使用场景主要包括:

  • 调试组件生命周期:帮助开发者观察组件的加载、更新和卸载时机,方便调试。
  • 追踪用户交互:通过记录特定操作(如按钮点击、表单提交)发生的时间点,来追踪用户的操作行为。
  • 性能优化:通过分析哪些组件频繁更新,确定哪些部分需要优化。

总结

withLogging 高阶组件通过增强现有组件的功能,使开发者可以方便地在不修改原有组件代码的情况下,添加日志记录功能。在项目中,通过这种方式可以更好地追踪组件的行为和生命周期,提升调试和开发效率。

友情赞助
大家有空闲,帮忙试用下,国货抖音出品的AI编程代码插件,比比看GitHub Copilot那个好**^-^**
(适用JetBrains全家桶系列、Visual Studio家族IDE工具安装等主流IDE工具,支持100+种编程语言)
帮忙去助力>>