MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

React 高阶组件基础与设计理念

2023-11-124.3k 阅读

React 高阶组件基础

什么是高阶组件

在 React 中,高阶组件(Higher - Order Component,简称 HOC)并不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。简单来说,高阶组件是一个函数,它接受一个组件作为参数,并返回一个新的组件。

用代码来表示就是这样:

import React from 'react';

// 高阶组件函数
function withNewFeature(WrappedComponent) {
    return class NewComponent extends React.Component {
        render() {
            return <WrappedComponent {...this.props} />;
        }
    };
}

// 被包装的组件
class MyComponent extends React.Component {
    render() {
        return <div>My Component</div>;
    }
}

// 使用高阶组件包装 MyComponent
const EnhancedComponent = withNewFeature(MyComponent);

在上述代码中,withNewFeature 就是一个高阶组件,它接收 MyComponent 作为参数,并返回了一个新的组件 NewComponent(在实际使用中 EnhancedComponent 就是这个新组件的引用)。这个新组件在 render 方法中渲染了传入的 WrappedComponent 并传递了所有的 props。

高阶组件的作用

  1. 代码复用:假设有多个组件需要添加相同的功能,比如添加日志记录功能。通过高阶组件,可以将这个功能封装在高阶组件中,然后将需要这个功能的组件传入高阶组件,从而实现代码复用。
import React from 'react';

// 日志记录高阶组件
function withLogging(WrappedComponent) {
    return class LoggingComponent extends React.Component {
        componentDidMount() {
            console.log(`${WrappedComponent.name} has mounted`);
        }
        componentWillUnmount() {
            console.log(`${WrappedComponent.name} is about to unmount`);
        }
        render() {
            return <WrappedComponent {...this.props} />;
        }
    };
}

// 组件 A
class ComponentA extends React.Component {
    render() {
        return <div>Component A</div>;
    }
}

// 组件 B
class ComponentB extends React.Component {
    render() {
        return <div>Component B</div>;
    }
}

// 使用高阶组件包装组件 A 和组件 B
const EnhancedComponentA = withLogging(ComponentA);
const EnhancedComponentB = withLogging(ComponentB);

在这个例子中,withLogging 高阶组件为 ComponentAComponentB 都添加了日志记录功能,避免了在每个组件中重复编写 componentDidMountcomponentWillUnmount 中的日志记录代码。

  1. 逻辑增强:高阶组件可以为传入的组件添加额外的状态或方法,从而增强组件的功能。比如,添加一个用于控制加载状态的高阶组件。
import React from'react';

// 加载状态高阶组件
function withLoading(WrappedComponent) {
    return class LoadingComponent extends React.Component {
        constructor(props) {
            super(props);
            this.state = {
                isLoading: false
            };
        }
        startLoading = () => {
            this.setState({ isLoading: true });
        };
        stopLoading = () => {
            this.setState({ isLoading: false });
        };
        render() {
            const { isLoading } = this.state;
            return <WrappedComponent {...this.props} isLoading={isLoading} startLoading={this.startLoading} stopLoading={this.stopLoading} />;
        }
    };
}

// 示例组件
class DataComponent extends React.Component {
    handleClick = () => {
        const { startLoading, stopLoading } = this.props;
        startLoading();
        // 模拟数据请求
        setTimeout(() => {
            stopLoading();
        }, 2000);
    };
    render() {
        const { isLoading } = this.props;
        return (
            <div>
                {isLoading? <p>Loading...</p> : <button onClick={this.handleClick}>Fetch Data</button>}
            </div>
        );
    }
}

// 使用高阶组件包装 DataComponent
const EnhancedDataComponent = withLoading(DataComponent);

在这个例子中,withLoading 高阶组件为 DataComponent 添加了加载状态管理功能,DataComponent 可以通过 startLoadingstopLoading 方法来控制加载状态,并在界面上显示相应的加载提示。

  1. 属性代理:高阶组件可以对传入的 props 进行操作,比如添加新的 props、修改已有的 props 或者过滤掉某些 props。
import React from'react';

// 属性代理高阶组件
function withExtraProps(WrappedComponent) {
    return class ExtraPropsComponent extends React.Component {
        render() {
            const newProps = {
               ...this.props,
                extraProp: 'This is an extra prop'
            };
            return <WrappedComponent {...newProps} />;
        }
    };
}

// 示例组件
class PropsComponent extends React.Component {
    render() {
        const { extraProp } = this.props;
        return <div>{extraProp}</div>;
    }
}

// 使用高阶组件包装 PropsComponent
const EnhancedPropsComponent = withExtraProps(PropsComponent);

在这个例子中,withExtraProps 高阶组件为 PropsComponent 添加了一个新的 extraProp 属性。

React 高阶组件设计理念

组合优于继承

在传统的面向对象编程中,继承是实现代码复用的常用方式。然而在 React 中,更推荐使用组合的方式,而高阶组件正是组合思想的一个重要体现。

以 React 组件为例,如果使用继承来实现代码复用,可能会出现以下问题:

  1. 紧密耦合:子类会紧密依赖父类的实现细节。如果父类的接口或实现发生变化,子类可能需要进行大量的修改。
  2. 多重继承问题:在一些语言中,多重继承会导致菱形继承问题,即一个子类从多个父类继承相同的属性或方法,导致命名冲突和难以维护的代码结构。虽然 JavaScript 不支持传统的多重继承,但通过原型链继承也可能会带来类似的复杂性。

相比之下,高阶组件通过组合的方式,将功能封装在独立的高阶组件中,然后通过传入不同的组件来复用这些功能。这种方式使得组件之间的耦合度更低,更加灵活和易于维护。

例如,假设有一个 BaseComponent 作为父类,有多个子类继承自它并添加不同的功能:

// 传统继承方式
class BaseComponent extends React.Component {
    commonMethod() {
        // 一些通用逻辑
    }
    render() {
        return <div>Base Component</div>;
    }
}

class SubComponent1 extends BaseComponent {
    extraMethod1() {
        // 额外功能 1
    }
    render() {
        return <div>{super.render()}</div>;
    }
}

class SubComponent2 extends BaseComponent {
    extraMethod2() {
        // 额外功能 2
    }
    render() {
        return <div>{super.render()}</div>;
    }
}

如果使用高阶组件来实现类似功能:

// 高阶组件方式
function withExtraFeature1(WrappedComponent) {
    return class EnhancedComponent1 extends React.Component {
        extraMethod1() {
            // 额外功能 1
        }
        render() {
            return <WrappedComponent {...this.props} />;
        }
    };
}

function withExtraFeature2(WrappedComponent) {
    return class EnhancedComponent2 extends React.Component {
        extraMethod2() {
            // 额外功能 2
        }
        render() {
            return <WrappedComponent {...this.props} />;
        }
    };
}

class MyBaseComponent extends React.Component {
    commonMethod() {
        // 一些通用逻辑
    }
    render() {
        return <div>My Base Component</div>;
    }
}

const ComponentWithFeature1 = withExtraFeature1(MyBaseComponent);
const ComponentWithFeature2 = withExtraFeature2(MyBaseComponent);

可以看到,通过高阶组件的组合方式,代码结构更加清晰,组件之间的依赖关系更加简单,不同的功能可以独立开发和复用。

单一职责原则

在设计高阶组件时,应该遵循单一职责原则。每个高阶组件应该只负责一个特定的功能,这样可以使高阶组件更加易于理解、测试和维护。

例如,前面提到的 withLogging 高阶组件只负责添加日志记录功能,withLoading 高阶组件只负责添加加载状态管理功能。如果将多个功能合并到一个高阶组件中,可能会导致该高阶组件变得臃肿和难以维护。

假设有一个高阶组件试图同时实现日志记录和加载状态管理功能:

import React from'react';

// 功能混杂的高阶组件
function withMixedFeatures(WrappedComponent) {
    return class MixedFeaturesComponent extends React.Component {
        constructor(props) {
            super(props);
            this.state = {
                isLoading: false
            };
        }
        startLoading = () => {
            this.setState({ isLoading: true });
        };
        stopLoading = () => {
            this.setState({ isLoading: false });
        };
        componentDidMount() {
            console.log(`${WrappedComponent.name} has mounted`);
        }
        componentWillUnmount() {
            console.log(`${WrappedComponent.name} is about to unmount`);
        }
        render() {
            const { isLoading } = this.state;
            return <WrappedComponent {...this.props} isLoading={isLoading} startLoading={this.startLoading} stopLoading={this.stopLoading} />;
        }
    };
}

这样的高阶组件在功能上虽然满足了需求,但如果后续需要单独修改日志记录功能或者加载状态管理功能,就会变得比较困难。而且,如果有其他组件只需要其中一个功能,也无法复用这个高阶组件。

因此,将功能拆分成多个单一职责的高阶组件是更好的选择:

import React from'react';

// 日志记录高阶组件
function withLogging(WrappedComponent) {
    return class LoggingComponent extends React.Component {
        componentDidMount() {
            console.log(`${WrappedComponent.name} has mounted`);
        }
        componentWillUnmount() {
            console.log(`${WrappedComponent.name} is about to unmount`);
        }
        render() {
            return <WrappedComponent {...this.props} />;
        }
    };
}

// 加载状态高阶组件
function withLoading(WrappedComponent) {
    return class LoadingComponent extends React.Component {
        constructor(props) {
            super(props);
            this.state = {
                isLoading: false
            };
        }
        startLoading = () => {
            this.setState({ isLoading: true });
        };
        stopLoading = () => {
            this.setState({ isLoading: false });
        };
        render() {
            const { isLoading } = this.state;
            return <WrappedComponent {...this.props} isLoading={isLoading} startLoading={this.startLoading} stopLoading={this.stopLoading} />;
        }
    };
}

class MyComponent extends React.Component {
    render() {
        return <div>My Component</div>;
    }
}

// 使用多个高阶组件
const ComponentWithLoggingAndLoading = withLoading(withLogging(MyComponent));

通过这种方式,每个高阶组件的职责明确,易于维护和复用。

保持组件的纯净性

高阶组件应该尽量保持传入组件的纯净性,即不改变传入组件的内部状态和行为,除非有明确的需求。

例如,高阶组件不应该直接修改传入组件的 state 或者在没有合理理由的情况下覆盖传入组件的方法。如果高阶组件需要给传入组件添加新的功能,可以通过 props 的方式传递新的方法或数据。

import React from'react';

// 错误示例:修改传入组件的 state
function badHOC(WrappedComponent) {
    return class BadEnhancedComponent extends React.Component {
        componentDidMount() {
            this.props.children.setState({ someNewState: 'This is wrong' });
        }
        render() {
            return <WrappedComponent {...this.props} />;
        }
    };
}

// 正确示例:通过 props 传递功能
function goodHOC(WrappedComponent) {
    return class GoodEnhancedComponent extends React.Component {
        newFunction = () => {
            // 一些新功能逻辑
        };
        render() {
            return <WrappedComponent {...this.props} newFunction={this.newFunction} />;
        }
    };
}

在错误示例中,badHOC 直接尝试修改传入组件(通过 this.props.children 假设是传入组件实例)的 state,这破坏了组件的封装性和纯净性。而在正确示例中,goodHOC 通过 props 传递了一个新的函数 newFunction,让传入组件可以根据需要使用这个新功能,保持了传入组件的纯净性。

高阶组件的透传 props

在设计高阶组件时,需要考虑如何透传 props。通常情况下,高阶组件应该将接收到的所有 props 透传给被包装的组件,除非有特殊的处理需求。

例如,前面的 withLogging 高阶组件:

import React from'react';

function withLogging(WrappedComponent) {
    return class LoggingComponent extends React.Component {
        componentDidMount() {
            console.log(`${WrappedComponent.name} has mounted`);
        }
        componentWillUnmount() {
            console.log(`${WrappedComponent.name} is about to unmount`);
        }
        render() {
            return <WrappedComponent {...this.props} />;
        }
    };
}

在这个例子中,{...this.props} 将所有接收到的 props 透传给了 WrappedComponent。这样,被包装的组件可以像没有被高阶组件包装一样正常接收和使用 props。

然而,有时候可能需要对 props 进行一些处理。比如,过滤掉某些 props:

import React from'react';

function filterProps(WrappedComponent) {
    return class FilteredPropsComponent extends React.Component {
        render() {
            const { unwantedProp,...filteredProps } = this.props;
            return <WrappedComponent {...filteredProps} />;
        }
    };
}

class MyComponent extends React.Component {
    render() {
        const { normalProp } = this.props;
        return <div>{normalProp}</div>;
    }
}

// 使用高阶组件
const EnhancedComponent = filterProps(MyComponent);
// 使用时传递 unwantedProp 和 normalProp
<EnhancedComponent unwantedProp="This will be filtered" normalProp="This is normal" />

在这个例子中,filterProps 高阶组件过滤掉了 unwantedProp,只将其他 props 透传给 MyComponent

高阶组件的命名规范

为了提高代码的可读性和可维护性,高阶组件应该遵循一定的命名规范。

  1. 前缀命名:通常,高阶组件的命名以 withenhance 等前缀开头,后面跟上描述该高阶组件功能的名称。例如,withLoggingwithLoadingenhanceDataFetching 等。这样命名可以清晰地表明这是一个高阶组件,并且能快速了解其功能。
  2. 避免与普通组件混淆:高阶组件的命名应该避免与普通 React 组件混淆。由于高阶组件本质上是函数,而普通组件是类或函数定义的 React 组件,通过特定的前缀命名可以很好地区分它们。

高阶组件与 React Hooks 的关系

React Hooks 是 React 16.8 引入的新特性,它为函数式组件带来了状态和副作用管理等能力。高阶组件和 React Hooks 在功能上有一些重叠之处,比如都可以用于复用组件逻辑。

  1. 功能重叠:例如,前面实现的 withLoading 高阶组件,可以通过 React Hooks 来实现类似的功能。
import React, { useState } from'react';

function useLoading() {
    const [isLoading, setIsLoading] = useState(false);
    const startLoading = () => setIsLoading(true);
    const stopLoading = () => setIsLoading(false);
    return { isLoading, startLoading, stopLoading };
}

function DataComponent() {
    const { isLoading, startLoading, stopLoading } = useLoading();
    const handleClick = () => {
        startLoading();
        // 模拟数据请求
        setTimeout(() => {
            stopLoading();
        }, 2000);
    };
    return (
        <div>
            {isLoading? <p>Loading...</p> : <button onClick={handleClick}>Fetch Data</button>}
        </div>
    );
}

在这个例子中,useLoading Hook 实现了与 withLoading 高阶组件类似的加载状态管理功能,并且直接在函数式组件 DataComponent 中使用。

  1. 优势互补:高阶组件和 React Hooks 也有各自的优势。高阶组件适用于类组件,并且可以对整个组件进行包装,实现更复杂的功能代理和逻辑增强。而 React Hooks 更适合函数式组件,它的语法更加简洁,并且可以在函数组件内部灵活地复用逻辑,避免了高阶组件可能带来的嵌套层级过深的问题。

在实际开发中,可以根据具体的场景和需求选择使用高阶组件或 React Hooks。对于一些已经存在的类组件,使用高阶组件来添加新功能可能更加方便;而对于新开发的函数式组件,React Hooks 可能是更好的选择。

总之,高阶组件是 React 中一种强大的设计模式,深入理解其基础和设计理念,能够帮助开发者更好地组织和复用代码,提高开发效率和代码质量。无论是在小型项目还是大型应用中,合理运用高阶组件都能使 React 应用的架构更加清晰和易于维护。同时,结合 React Hooks 等新特性,可以进一步提升 React 开发的灵活性和便捷性。