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

React 中高阶组件对 Props 和 State 的封装

2021-03-196.3k 阅读

一、高阶组件概述

在 React 开发中,高阶组件(Higher - Order Component,HOC)是一种极为强大的设计模式。它本质上是一个函数,该函数接收一个组件作为参数,并返回一个新的组件。这种模式在不修改原有组件代码的前提下,为组件添加额外的功能,极大地增强了代码的复用性和可维护性。

(一)高阶组件的基本概念

高阶组件并不属于 React API 的一部分,它是基于 React 的组合特性而形成的一种约定俗成的模式。通过高阶组件,我们可以将一些通用的逻辑抽离出来,应用到多个不同的组件上。例如,权限验证、数据获取等逻辑,原本可能需要在多个组件中重复编写,使用高阶组件后,可以将这些逻辑封装在高阶组件内,不同组件只需通过高阶组件包裹,就能获得相应的功能。

(二)高阶组件的类型

  1. 属性代理(Props Proxy) 属性代理型高阶组件通过包裹传入的组件,对传入组件的 props 进行操作,然后将新的 props 传递给被包裹的组件。这种类型的高阶组件可以添加、修改或删除 props,还可以监听 props 的变化。
  2. 反向继承(Inheritance Inversion) 反向继承型高阶组件通过继承传入的组件来创建新的组件。在新组件中,可以重写或扩展被继承组件的生命周期方法、render 方法等。这种类型的高阶组件可以访问和修改被包裹组件的 state 和生命周期,但可能会导致一些问题,比如难以调试,因为新组件和被包裹组件的关系通过继承变得复杂。

二、高阶组件对 Props 的封装

(一)Props 的增强与修改

  1. 添加新的 Props 在很多场景下,我们需要为组件添加一些通用的 props,而无需在每个使用该组件的地方都手动添加。例如,我们有一个展示用户信息的组件 UserInfo,可能希望为它添加一个 data - testid 属性,方便在测试时定位组件。
import React from'react';

// 高阶组件,为传入的组件添加 data - testid 属性
const withTestId = (WrappedComponent) => {
    return (props) => {
        return <WrappedComponent {...props} data - testid="test - id - for - component" />;
    };
};

const UserInfo = ({ name, age }) => {
    return (
        <div>
            <p>Name: {name}</p>
            <p>Age: {age}</p>
        </div>
    );
};

const EnhancedUserInfo = withTestId(UserInfo);

export default EnhancedUserInfo;

在上述代码中,withTestId 是一个高阶组件,它接收一个组件 WrappedComponent,返回一个新的组件。在新组件的 render 方法中,除了传递原有的 props 外,还添加了 data - testid 属性。

  1. 修改现有的 Props 有时候,我们需要根据一些条件修改组件接收到的 props。比如,有一个接收用户年龄的组件,我们希望在传递给实际渲染组件之前,将年龄小于 0 的值修正为 0。
import React from'react';

const fixNegativeAge = (WrappedComponent) => {
    return (props) => {
        let newProps = {...props };
        if (props.age < 0) {
            newProps.age = 0;
        }
        return <WrappedComponent {...newProps} />;
    };
};

const AgeDisplay = ({ age }) => {
    return <p>Age: {age}</p>;
};

const FixedAgeDisplay = fixNegativeAge(AgeDisplay);

export default FixedAgeDisplay;

在这个例子中,fixNegativeAge 高阶组件会检查传入的 age prop,如果 age 小于 0,则将其修正为 0,然后将修正后的 props 传递给被包裹的组件 AgeDisplay

(二)Props 的控制与过滤

  1. 过滤特定的 Props 假设我们有一个组件,它接收很多 props,但我们只想让其中一部分 props 传递给实际渲染的子组件。例如,有一个通用的 Form 组件,它接收各种表单相关的 props 以及一些额外的配置 props,我们只想将与表单输入相关的 props 传递给表单输入子组件。
import React from'react';

const filterFormProps = (WrappedComponent) => {
    return (props) => {
        const formProps = ['name', 'value', 'onChange'];
        const filteredProps = {};
        formProps.forEach(prop => {
            if (props.hasOwnProperty(prop)) {
                filteredProps[prop] = props[prop];
            }
        });
        return <WrappedComponent {...filteredProps} />;
    };
};

const InputField = ({ name, value, onChange }) => {
    return <input type="text" name={name} value={value} onChange={onChange} />;
};

const FilteredInputField = filterFormProps(InputField);

export default FilteredInputField;

这里,filterFormProps 高阶组件会从传入的 props 中过滤出 namevalueonChange 这几个与表单输入相关的 props,并传递给 InputField 组件。

  1. 条件性地传递 Props 我们可能需要根据某些条件来决定是否传递某个 prop。比如,有一个按钮组件,我们希望根据用户的权限来决定是否显示禁用属性。
import React from'react';

const withPermission = (WrappedComponent) => {
    return (props) => {
        const { hasPermission, ...otherProps } = props;
        let newProps = otherProps;
        if (!hasPermission) {
            newProps.disabled = true;
        }
        return <WrappedComponent {...newProps} />;
    };
};

const ActionButton = ({ label, onClick, disabled }) => {
    return <button onClick={onClick} disabled={disabled}>{label}</button>;
};

const PermissionedButton = withPermission(ActionButton);

export default PermissionedButton;

在上述代码中,withPermission 高阶组件根据 hasPermission prop 的值来决定是否为 ActionButton 组件添加 disabled prop。

三、高阶组件对 State 的封装

(一)为组件添加 State

  1. 简单的 State 添加 有时,我们希望为一个无状态组件添加一些状态。例如,有一个展示图片的组件,我们希望添加一个点击切换图片的功能,这就需要为它添加一个状态来记录当前显示的图片索引。
import React from'react';

const withImageToggle = (WrappedComponent) => {
    return class extends React.Component {
        constructor(props) {
            super(props);
            this.state = {
                currentIndex: 0
            };
            this.handleClick = this.handleClick.bind(this);
        }
        handleClick() {
            this.setState((prevState) => ({
                currentIndex: prevState.currentIndex === 0? 1 : 0
            }));
        }
        render() {
            const { currentIndex } = this.state;
            const newProps = {
               ...this.props,
                currentIndex,
                handleClick: this.handleClick
            };
            return <WrappedComponent {...newProps} />;
        }
    };
};

const ImageDisplay = ({ images, currentIndex, handleClick }) => {
    return (
        <div>
            <img src={images[currentIndex]} alt="display" />
            <button onClick={handleClick}>Toggle Image</button>
        </div>
    );
};

const ToggledImageDisplay = withImageToggle(ImageDisplay);

export default ToggledImageDisplay;

在这个例子中,withImageToggle 高阶组件通过继承创建了一个新的有状态组件。它为 ImageDisplay 组件添加了 currentIndex 状态和 handleClick 方法,使得 ImageDisplay 组件能够实现图片切换功能。

  1. 复杂 State 管理 对于更复杂的状态管理,比如涉及到异步操作的状态,我们也可以通过高阶组件来封装。例如,有一个组件需要从 API 获取数据,并且在加载过程中有加载状态,获取成功或失败有相应的提示。
import React from'react';

const withDataFetching = (WrappedComponent, fetchData) => {
    return class extends React.Component {
        constructor(props) {
            super(props);
            this.state = {
                data: null,
                isLoading: false,
                error: null
            };
            this.fetchData = this.fetchData.bind(this);
        }
        async fetchData() {
            this.setState({ isLoading: true });
            try {
                const response = await fetchData();
                this.setState({ data: response, isLoading: false });
            } catch (error) {
                this.setState({ error, isLoading: false });
            }
        }
        componentDidMount() {
            this.fetchData();
        }
        render() {
            const { data, isLoading, error } = this.state;
            const newProps = {
               ...this.props,
                data,
                isLoading,
                error,
                refetch: this.fetchData
            };
            return <WrappedComponent {...newProps} />;
        }
    };
};

const DataDisplay = ({ data, isLoading, error, refetch }) => {
    if (isLoading) {
        return <p>Loading...</p>;
    }
    if (error) {
        return <p>Error: {error.message}</p>;
    }
    return (
        <div>
            <p>{JSON.stringify(data)}</p>
            <button onClick={refetch}>Refetch</button>
        </div>
    );
};

const fetchUserData = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve({ name: 'John', age: 30 });
        }, 1000);
    });
};

const EnhancedDataDisplay = withDataFetching(DataDisplay, fetchUserData);

export default EnhancedDataDisplay;

在上述代码中,withDataFetching 高阶组件为 DataDisplay 组件添加了数据获取相关的状态(dataisLoadingerror)和方法(refetch)。fetchData 方法负责异步获取数据,并根据结果更新状态。

(二)控制组件 State 的变化

  1. 限制 State 的修改 有时,我们希望对组件的 state 修改进行一定的控制。比如,有一个计数器组件,我们希望只能通过特定的方法来增加或减少计数,而不能随意修改 state。
import React from'react';

const controlledCounter = (WrappedComponent) => {
    return class extends React.Component {
        constructor(props) {
            super(props);
            this.state = {
                count: 0
            };
            this.increment = this.increment.bind(this);
            this.decrement = this.decrement.bind(this);
        }
        increment() {
            this.setState((prevState) => ({ count: prevState.count + 1 }));
        }
        decrement() {
            this.setState((prevState) => ({ count: prevState.count - 1 }));
        }
        render() {
            const { count } = this.state;
            const newProps = {
               ...this.props,
                count,
                increment: this.increment,
                decrement: this.decrement
            };
            return <WrappedComponent {...newProps} />;
        }
    };
};

const CounterDisplay = ({ count, increment, decrement }) => {
    return (
        <div>
            <p>Count: {count}</p>
            <button onClick={increment}>Increment</button>
            <button onClick={decrement}>Decrement</button>
        </div>
    );
};

const ControlledCounterDisplay = controlledCounter(CounterDisplay);

export default ControlledCounterDisplay;

在这个例子中,controlledCounter 高阶组件封装了 CounterDisplay 组件的 state 修改逻辑,只通过 incrementdecrement 方法来修改 count 状态,避免了外部对 count 状态的直接修改。

  1. 监听 State 的变化 我们可能需要在组件的 state 发生变化时执行一些额外的操作。例如,有一个输入框组件,当输入框的值(state)发生变化时,我们希望记录变化日志。
import React from'react';

const logInputChange = (WrappedComponent) => {
    return class extends React.Component {
        constructor(props) {
            super(props);
            this.state = {
                value: ''
            };
            this.handleChange = this.handleChange.bind(this);
        }
        handleChange(e) {
            const newVal = e.target.value;
            console.log(`Value changed to: ${newVal}`);
            this.setState({ value: newVal });
        }
        render() {
            const { value } = this.state;
            const newProps = {
               ...this.props,
                value,
                onChange: this.handleChange
            };
            return <WrappedComponent {...newProps} />;
        }
    };
};

const InputBox = ({ value, onChange }) => {
    return <input type="text" value={value} onChange={onChange} />;
};

const LoggedInputBox = logInputChange(InputBox);

export default LoggedInputBox;

这里,logInputChange 高阶组件在 handleChange 方法中不仅更新了 state,还记录了输入框值的变化日志,实现了对 state 变化的监听。

四、高阶组件封装 Props 和 State 的综合应用

(一)权限控制与数据加载结合

在实际项目中,我们经常需要结合多种功能。比如,一个需要权限验证并且加载用户数据的组件。

import React from'react';

const withPermission = (WrappedComponent) => {
    return (props) => {
        const { hasPermission, ...otherProps } = props;
        if (!hasPermission) {
            return <p>You don't have permission</p>;
        }
        return <WrappedComponent {...otherProps} />;
    };
};

const withDataFetching = (WrappedComponent, fetchData) => {
    return class extends React.Component {
        constructor(props) {
            super(props);
            this.state = {
                data: null,
                isLoading: false,
                error: null
            };
            this.fetchData = this.fetchData.bind(this);
        }
        async fetchData() {
            this.setState({ isLoading: true });
            try {
                const response = await fetchData();
                this.setState({ data: response, isLoading: false });
            } catch (error) {
                this.setState({ error, isLoading: false });
            }
        }
        componentDidMount() {
            this.fetchData();
        }
        render() {
            const { data, isLoading, error } = this.state;
            const newProps = {
               ...this.props,
                data,
                isLoading,
                error,
                refetch: this.fetchData
            };
            return <WrappedComponent {...newProps} />;
        }
    };
};

const fetchUserData = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve({ name: 'John', age: 30 });
        }, 1000);
    });
};

const UserDataDisplay = ({ data, isLoading, error, refetch }) => {
    if (isLoading) {
        return <p>Loading...</p>;
    }
    if (error) {
        return <p>Error: {error.message}</p>;
    }
    return (
        <div>
            <p>{JSON.stringify(data)}</p>
            <button onClick={refetch}>Refetch</button>
        </div>
    );
};

const EnhancedUserDataDisplay = withPermission(withDataFetching(UserDataDisplay, fetchUserData));

export default EnhancedUserDataDisplay;

在这个例子中,withPermission 高阶组件先进行权限验证,只有在用户有权限时,才会让 withDataFetching 高阶组件去加载用户数据并展示。

(二)多高阶组件嵌套与冲突处理

  1. 多高阶组件嵌套 当我们使用多个高阶组件时,可能会进行嵌套使用。例如,一个组件可能同时需要添加测试 ID、进行数据加载和权限控制。
import React from'react';

const withTestId = (WrappedComponent) => {
    return (props) => {
        return <WrappedComponent {...props} data - testid="test - id - for - component" />;
    };
};

const withPermission = (WrappedComponent) => {
    return (props) => {
        const { hasPermission, ...otherProps } = props;
        if (!hasPermission) {
            return <p>You don't have permission</p>;
        }
        return <WrappedComponent {...otherProps} />;
    };
};

const withDataFetching = (WrappedComponent, fetchData) => {
    return class extends React.Component {
        constructor(props) {
            super(props);
            this.state = {
                data: null,
                isLoading: false,
                error: null
            };
            this.fetchData = this.fetchData.bind(this);
        }
        async fetchData() {
            this.setState({ isLoading: true });
            try {
                const response = await fetchData();
                this.setState({ data: response, isLoading: false });
            } catch (error) {
                this.setState({ error, isLoading: false });
            }
        }
        componentDidMount() {
            this.fetchData();
        }
        render() {
            const { data, isLoading, error } = this.state;
            const newProps = {
               ...this.props,
                data,
                isLoading,
                error,
                refetch: this.fetchData
            };
            return <WrappedComponent {...newProps} />;
        }
    };
};

const fetchUserData = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve({ name: 'John', age: 30 });
        }, 1000);
    });
};

const UserDataDisplay = ({ data, isLoading, error, refetch }) => {
    if (isLoading) {
        return <p>Loading...</p>;
    }
    if (error) {
        return <p>Error: {error.message}</p>;
    }
    return (
        <div>
            <p>{JSON.stringify(data)}</p>
            <button onClick={refetch}>Refetch</button>
        </div>
    );
};

const FullyEnhancedComponent = withTestId(withPermission(withDataFetching(UserDataDisplay, fetchUserData)));

export default FullyEnhancedComponent;

在上述代码中,我们将 withTestIdwithPermissionwithDataFetching 三个高阶组件进行了嵌套使用,使得 UserDataDisplay 组件同时具备添加测试 ID、权限验证和数据加载的功能。

  1. 冲突处理 在多高阶组件嵌套时,可能会出现 props 冲突或逻辑冲突的情况。比如,两个高阶组件都试图修改同一个 prop。为了避免这种情况,我们需要在编写高阶组件时遵循一些规范。例如,为高阶组件添加的 prop 使用特定的命名前缀,或者在高阶组件内部对 prop 的修改进行更细致的判断。
import React from'react';

const prefixProps = (WrappedComponent, prefix) => {
    return (props) => {
        const newProps = {};
        Object.keys(props).forEach(key => {
            if (key.startsWith(prefix)) {
                newProps[key.replace(prefix, '')] = props[key];
            }
        });
        return <WrappedComponent {...newProps} />;
    };
};

const modifyProp = (WrappedComponent) => {
    return (props) => {
        let newProps = {...props };
        if (newProps.someProp) {
            newProps.someProp = 'Modified value';
        }
        return <WrappedComponent {...newProps} />;
    };
};

const ComponentWithProps = ({ someProp }) => {
    return <p>{someProp}</p>;
};

const prefixedComponent = prefixProps(ComponentWithProps, 'custom - ');
const modifiedComponent = modifyProp(prefixedComponent);

export default modifiedComponent;

在这个例子中,prefixProps 高阶组件对带有特定前缀的 prop 进行处理,避免了与其他高阶组件对 prop 修改时可能产生的冲突。通过这种方式,可以在多高阶组件嵌套时更优雅地管理 props 和逻辑。

五、高阶组件封装 Props 和 State 的注意事项

(一)性能问题

  1. 不必要的渲染 高阶组件可能会导致不必要的渲染。例如,当高阶组件返回的新组件的 props 没有变化,但由于其自身的 state 变化,可能会触发被包裹组件的重新渲染。为了避免这种情况,可以使用 React.memo 对被包裹组件进行包裹,这样只有当 props 真正发生变化时,被包裹组件才会重新渲染。
import React from'react';

const withExtraState = (WrappedComponent) => {
    return class extends React.Component {
        constructor(props) {
            super(props);
            this.state = {
                someValue: 0
            };
            this.incrementValue = this.incrementValue.bind(this);
        }
        incrementValue() {
            this.setState((prevState) => ({ someValue: prevState.someValue + 1 }));
        }
        render() {
            const newProps = {
               ...this.props,
                incrementValue: this.incrementValue
            };
            return <WrappedComponent {...newProps} />;
        }
    };
};

const MemoizedComponent = React.memo(({ incrementValue }) => {
    return <button onClick={incrementValue}>Increment</button>;
});

const EnhancedMemoizedComponent = withExtraState(MemoizedComponent);

export default EnhancedMemoizedComponent;

在上述代码中,MemoizedComponent 使用 React.memo 进行包裹,只有当 incrementValue prop 变化时才会重新渲染,避免了因高阶组件 state 变化而导致的不必要渲染。

  1. 渲染次数过多 如果高阶组件嵌套层数过多,可能会导致渲染次数呈指数级增长,严重影响性能。因此,在设计高阶组件时,要尽量减少不必要的嵌套,确保逻辑清晰且性能优化。

(二)调试问题

  1. 组件结构复杂 高阶组件的使用会使组件结构变得复杂,尤其是在多高阶组件嵌套的情况下。调试时,很难确定问题出在哪一层高阶组件或被包裹组件中。为了解决这个问题,可以在每个高阶组件内部添加一些调试信息,比如在 console.log 中输出组件的名称和当前的 propsstate
import React from'react';

const withDebugInfo = (WrappedComponent) => {
    return (props) => {
        console.log(`Props for ${WrappedComponent.name}:`, props);
        return <WrappedComponent {...props} />;
    };
};

const SimpleComponent = ({ message }) => {
    return <p>{message}</p>;
};

const DebuggedSimpleComponent = withDebugInfo(SimpleComponent);

export default DebuggedSimpleComponent;

在这个例子中,withDebugInfo 高阶组件会在控制台输出传递给 SimpleComponentprops,方便调试时查看。

  1. 难以追踪 State 和 Props 变化 由于高阶组件对 propsstate 进行了封装和修改,追踪它们的变化变得困难。可以使用 React DevTools 来辅助调试,它可以清晰地展示组件树结构以及每个组件的 propsstate 变化情况。同时,在高阶组件中添加一些自定义的日志记录方法,记录 propsstate 的关键变化点,也有助于调试。

(三)与 React 新特性的兼容性

  1. React Hooks 与高阶组件 随着 React Hooks 的出现,一些原本通过高阶组件实现的功能可以用 Hooks 更简洁地实现。例如,状态管理和副作用操作。然而,高阶组件在某些场景下仍然有其优势,比如需要在多个组件之间共享复杂逻辑,或者需要对组件的渲染进行更精细的控制。在使用高阶组件时,要注意与 Hooks 的兼容性,避免出现重复的逻辑或冲突。
  2. 新的 React 版本特性 React 不断更新迭代,新的版本可能会引入一些特性或改变组件的行为。在使用高阶组件时,要关注官方文档和更新日志,确保高阶组件与新特性兼容,并且不会因为 React 版本的升级而出现问题。例如,新的渲染模式(如 React 18 的并发模式)可能会影响高阶组件中一些生命周期方法的使用,需要及时调整高阶组件的代码以适应新的变化。