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

React 高阶组件在表单处理中的实践

2022-01-073.2k 阅读

1. React 高阶组件基础回顾

1.1 高阶组件定义

高阶组件(Higher - Order Component,HOC)在 React 中是一种设计模式,它本质上是一个函数,该函数接收一个组件作为参数,并返回一个新的组件。这种模式允许我们在不改变原始组件代码的情况下,对其功能进行增强。例如,我们有一个简单的 MyComponent

import React from'react';

const MyComponent = () => {
    return <div>这是 MyComponent</div>;
};

export default MyComponent;

我们可以创建一个高阶组件 withLogging 来增强它:

import React from'react';

const withLogging = (WrappedComponent) => {
    return (props) => {
        console.log('组件即将渲染');
        return <WrappedComponent {...props} />;
    };
};

export default withLogging;

使用高阶组件 withLogging 来增强 MyComponent

import React from'react';
import MyComponent from './MyComponent';
import withLogging from './withLogging';

const EnhancedComponent = withLogging(MyComponent);

const App = () => {
    return (
        <div>
            <EnhancedComponent />
        </div>
    );
};

export default App;

在上述代码中,withLogging 就是一个高阶组件,它接收 MyComponent 作为参数,并返回一个新的组件 EnhancedComponent,在 EnhancedComponent 渲染前,会打印日志信息。

1.2 高阶组件特性

  • 不改变原始组件:高阶组件不会直接修改传入的组件,而是返回一个新的增强后的组件。这使得原始组件保持纯净和可复用性。
  • 增强功能:通过包裹原始组件,高阶组件可以为其添加新的功能,如上述的日志记录功能,还可以添加数据获取、状态管理等功能。
  • 复用逻辑:高阶组件允许我们将一些通用的逻辑提取出来,复用在多个不同的组件上。比如,多个组件都需要进行权限验证,我们可以创建一个权限验证的高阶组件,应用到这些组件上。

2. 表单处理在 React 中的常见模式

2.1 受控组件

在 React 中,表单元素如 <input><textarea><select> 通常是受控组件。受控组件是指其值由 React 的状态来控制的组件。例如,一个简单的输入框:

import React, { useState } from'react';

const ControlledInput = () => {
    const [value, setValue] = useState('');

    const handleChange = (e) => {
        setValue(e.target.value);
    };

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

export default ControlledInput;

在这个例子中,input 元素的值由 value 状态控制,每次输入框的值变化时,handleChange 函数会更新 value 状态,从而实现对输入框的控制。

2.2 非受控组件

非受控组件则是使用 ref 来获取表单元素的值,而不是通过 React 的状态来控制。例如:

import React, { useRef } from'react';

const UncontrolledInput = () => {
    const inputRef = useRef();

    const handleSubmit = (e) => {
        e.preventDefault();
        console.log('输入的值为:', inputRef.current.value);
    };

    return (
        <form onSubmit={handleSubmit}>
            <input type="text" ref={inputRef} />
            <button type="submit">提交</button>
        </form>
    );
};

export default UncontrolledInput;

这里通过 ref 获取 input 元素的值,在提交表单时打印出来。非受控组件在某些场景下,如文件上传等,使用起来更为方便。

2.3 表单验证

表单验证是表单处理中重要的一部分。对于受控组件,我们可以在 onChange 事件处理函数中进行验证。例如,验证输入是否为数字:

import React, { useState } from'react';

const NumberInput = () => {
    const [value, setValue] = useState('');
    const [error, setError] = useState('');

    const handleChange = (e) => {
        const inputValue = e.target.value;
        if (/^\d+$/.test(inputValue)) {
            setValue(inputValue);
            setError('');
        } else {
            setError('请输入数字');
        }
    };

    return (
        <div>
            <input
                type="text"
                value={value}
                onChange={handleChange}
            />
            {error && <span style={{ color:'red' }}>{error}</span>}
        </div>
    );
};

export default NumberInput;

在这个例子中,当输入值为数字时,更新 value 状态并清除错误信息;否则,设置错误信息并显示在页面上。

3. 高阶组件在表单处理中的优势

3.1 复用表单逻辑

假设我们有多个表单组件,每个组件都需要进行类似的验证逻辑,如必填项验证。通过高阶组件,我们可以将这个验证逻辑提取出来,复用在多个组件上。例如,创建一个必填项验证的高阶组件 withRequired

import React from'react';

const withRequired = (WrappedComponent) => {
    return (props) => {
        const [error, setError] = React.useState('');

        const handleChange = (e) => {
            const inputValue = e.target.value;
            if (inputValue.trim() === '') {
                setError('该项为必填项');
            } else {
                setError('');
            }
            if (props.onChange) {
                props.onChange(e);
            }
        };

        return (
            <WrappedComponent
                {...props}
                error={error}
                onChange={handleChange}
            />
        );
    };
};

export default withRequired;

然后我们可以将这个高阶组件应用到输入框组件上:

import React from'react';
import withRequired from './withRequired';

const RequiredInput = () => {
    return (
        <input type="text" />
    );
};

const EnhancedRequiredInput = withRequired(RequiredInput);

const App = () => {
    return (
        <div>
            <EnhancedRequiredInput />
        </div>
    );
};

export default App;

这样,多个输入框组件都可以复用这个必填项验证逻辑,而不需要在每个组件内部重复编写验证代码。

3.2 集中管理表单状态

使用高阶组件可以将表单的状态管理逻辑集中起来。例如,我们可以创建一个高阶组件 withFormState,它可以管理表单的整体状态,如是否提交成功、是否有错误等。

import React from'react';

const withFormState = (WrappedComponent) => {
    return (props) => {
        const [isSubmitted, setIsSubmitted] = React.useState(false);
        const [formError, setFormError] = React.useState('');

        const handleSubmit = (e) => {
            e.preventDefault();
            // 模拟表单提交逻辑
            if (Math.random() > 0.5) {
                setIsSubmitted(true);
                setFormError('');
            } else {
                setIsSubmitted(false);
                setFormError('提交失败,请重试');
            }
        };

        return (
            <WrappedComponent
                {...props}
                isSubmitted={isSubmitted}
                formError={formError}
                handleSubmit={handleSubmit}
            />
        );
    };
};

export default withFormState;

然后在表单组件中使用这个高阶组件:

import React from'react';
import withFormState from './withFormState';

const MyForm = () => {
    return (
        <form>
            <input type="text" />
            <button type="submit">提交</button>
        </form>
    );
};

const EnhancedMyForm = withFormState(MyForm);

const App = () => {
    return (
        <div>
            <EnhancedMyForm />
        </div>
    );
};

export default App;

通过这种方式,表单的状态管理逻辑被封装在高阶组件中,使得表单组件本身更加简洁,并且易于维护和扩展。

3.3 分离业务逻辑与展示逻辑

高阶组件有助于将表单的业务逻辑(如验证、提交等)与展示逻辑(如表单的布局、样式等)分离。以一个复杂的注册表单为例,注册表单可能包含用户名、密码、邮箱等多个输入框以及验证和提交逻辑。我们可以使用高阶组件将验证和提交逻辑提取出来,只保留表单的展示部分在原始组件中。这样,当业务逻辑发生变化时,只需要修改高阶组件,而不需要修改展示组件,提高了代码的可维护性和可扩展性。

4. 高阶组件在表单处理中的实践案例

4.1 表单验证高阶组件实践

4.1.1 创建通用验证高阶组件

我们来创建一个更通用的验证高阶组件,它可以接收不同的验证规则。

import React from'react';

const withValidation = (validationRules) => {
    return (WrappedComponent) => {
        return (props) => {
            const [errors, setErrors] = React.useState({});

            const handleChange = (e) => {
                const newErrors = {};
                const inputName = e.target.name;
                for (const rule in validationRules) {
                    if (validationRules.hasOwnProperty(rule) && rule === inputName) {
                        const inputValue = e.target.value;
                        if (validationRules[rule].required && inputValue.trim() === '') {
                            newErrors[inputName] = '该项为必填项';
                        } else if (validationRules[rule].pattern &&!validationRules[rule].pattern.test(inputValue)) {
                            newErrors[inputName] = '输入格式不正确';
                        }
                    }
                }
                setErrors(newErrors);
                if (props.onChange) {
                    props.onChange(e);
                }
            };

            return (
                <WrappedComponent
                    {...props}
                    errors={errors}
                    onChange={handleChange}
                />
            );
        };
    };
};

export default withValidation;

4.1.2 使用验证高阶组件

假设我们有一个登录表单组件:

import React from'react';
import withValidation from './withValidation';

const LoginForm = () => {
    return (
        <form>
            <label>
                用户名:
                <input type="text" name="username" />
            </label>
            <label>
                密码:
                <input type="password" name="password" />
            </label>
            <button type="submit">登录</button>
        </form>
    );
};

const validationRules = {
    username: { required: true },
    password: { required: true, pattern: /^.{6,}$/ }
};

const EnhancedLoginForm = withValidation(validationRules)(LoginForm);

const App = () => {
    return (
        <div>
            <EnhancedLoginForm />
        </div>
    );
};

export default App;

在这个例子中,withValidation 高阶组件接收 validationRules 作为参数,然后应用到 LoginForm 上。当输入框的值发生变化时,会根据验证规则进行验证,并将错误信息传递给 LoginForm 组件,LoginForm 组件可以根据这些错误信息进行展示。

4.2 表单提交与状态管理高阶组件实践

4.2.1 创建表单提交与状态管理高阶组件

import React from'react';

const withFormSubmission = (WrappedComponent) => {
    return (props) => {
        const [isLoading, setIsLoading] = React.useState(false);
        const [submissionStatus, setSubmissionStatus] = React.useState('');

        const handleSubmit = async (e) => {
            e.preventDefault();
            setIsLoading(true);
            try {
                // 模拟异步提交
                await new Promise((resolve) => setTimeout(resolve, 2000));
                setSubmissionStatus('提交成功');
            } catch (error) {
                setSubmissionStatus('提交失败');
            } finally {
                setIsLoading(false);
            }
        };

        return (
            <WrappedComponent
                {...props}
                isLoading={isLoading}
                submissionStatus={submissionStatus}
                handleSubmit={handleSubmit}
            />
        );
    };
};

export default withFormSubmission;

4.2.2 使用表单提交与状态管理高阶组件

假设有一个注册表单组件:

import React from'react';
import withFormSubmission from './withFormSubmission';

const RegisterForm = () => {
    return (
        <form>
            <input type="text" placeholder="用户名" />
            <input type="password" placeholder="密码" />
            <button type="submit">注册</button>
            {props.isLoading && <span>正在提交...</span>}
            {props.submissionStatus && <span>{props.submissionStatus}</span>}
        </form>
    );
};

const EnhancedRegisterForm = withFormSubmission(RegisterForm);

const App = () => {
    return (
        <div>
            <EnhancedRegisterForm />
        </div>
    );
};

export default App;

在这个例子中,withFormSubmission 高阶组件为 RegisterForm 组件添加了提交状态管理功能。当点击提交按钮时,isLoading 状态变为 true,显示加载提示;提交完成后,根据结果设置 submissionStatus 状态,并显示相应的提示信息。

5. 高阶组件在表单处理中的注意事项

5.1 避免不必要的重渲染

高阶组件返回的新组件可能会因为某些原因导致不必要的重渲染。例如,如果高阶组件内部使用了 React.useStateReact.useEffect,并且依赖项没有正确设置,可能会导致在不需要更新的时候组件重新渲染。为了避免这种情况,我们需要仔细分析高阶组件内部的状态和副作用逻辑,确保依赖项的设置是准确的。例如,在上述 withFormSubmission 高阶组件中,handleSubmit 函数内部的异步操作不会导致不必要的重渲染,因为 await 会暂停函数执行,直到 Promise 被解决,并且 setIsLoadingsetSubmissionStatus 的更新都是在异步操作完成后进行的。

5.2 处理 props 冲突

当高阶组件为包裹的组件传递新的 props 时,可能会与原始组件的 props 发生冲突。例如,原始组件已经有一个名为 onChange 的 prop,而高阶组件也传递了一个 onChange prop。为了避免这种冲突,我们可以在高阶组件中对 prop 名称进行修改,或者在传递 prop 时进行判断和合并。在 withValidation 高阶组件中,我们通过将高阶组件内部的 handleChange 函数传递给包裹组件的 onChange prop,并且在 handleChange 函数内部调用了原始的 props.onChange(如果存在),这样既保证了验证逻辑的执行,又不会覆盖原始组件的 onChange 功能。

5.3 保持高阶组件的纯净性

高阶组件应该保持纯净,即不应该修改传入的组件,也不应该有副作用(除了返回新组件)。如果高阶组件修改了传入的组件,会破坏组件的可复用性和可维护性。例如,我们不能在高阶组件内部直接修改包裹组件的原型。同时,高阶组件内部的副作用应该尽量限制在状态更新和 DOM 操作等必要的范围内,避免引入与表单处理无关的复杂副作用,如全局变量的修改等。

6. 结合其他技术提升表单处理体验

6.1 与 Redux 结合

在大型应用中,表单数据可能需要与全局状态管理库如 Redux 结合。通过高阶组件,我们可以方便地将 Redux 的状态和 actions 注入到表单组件中。例如,我们可以创建一个高阶组件 withReduxForm,它使用 react - redux 库的 connect 函数来连接 Redux 状态和表单组件。

import { connect } from'react - redux';

const withReduxForm = (mapStateToProps, mapDispatchToProps) => {
    return (WrappedComponent) => {
        return connect(mapStateToProps, mapDispatchToProps)(WrappedComponent);
    };
};

export default withReduxForm;

然后在表单组件中使用这个高阶组件:

import React from'react';
import withReduxForm from './withReduxForm';
import { submitForm } from './actions';

const MyReduxForm = () => {
    return (
        <form>
            <input type="text" />
            <button type="submit">提交</button>
        </form>
    );
};

const mapStateToProps = (state) => {
    return {
        formData: state.formData
    };
};

const mapDispatchToProps = {
    submitForm
};

const EnhancedMyReduxForm = withReduxForm(mapStateToProps, mapDispatchToProps)(MyReduxForm);

export default EnhancedMyReduxForm;

在这个例子中,withReduxForm 高阶组件使用 connect 函数将 Redux 的状态 formData 和 action submitForm 注入到 MyReduxForm 组件中,使得表单组件可以与 Redux 进行交互。

6.2 使用 Formik 简化表单处理

Formik 是一个流行的 React 表单库,它提供了一种简洁的方式来处理表单的状态、验证和提交。我们可以结合高阶组件和 Formik 来进一步提升表单处理的效率。例如,我们可以创建一个高阶组件 withFormikEnhancement,它使用 Formik 的功能来增强表单组件。

import React from'react';
import { Formik } from 'formik';

const withFormikEnhancement = (initialValues, validationSchema) => {
    return (WrappedComponent) => {
        return (props) => {
            return (
                <Formik
                    initialValues={initialValues}
                    validationSchema={validationSchema}
                    onSubmit={(values, { setSubmitting }) => {
                        // 模拟表单提交逻辑
                        setTimeout(() => {
                            console.log('表单提交成功:', values);
                            setSubmitting(false);
                        }, 1000);
                    }}
                >
                    {({ errors, touched, handleChange, handleBlur, handleSubmit, isSubmitting }) => (
                        <WrappedComponent
                            {...props}
                            errors={errors}
                            touched={touched}
                            handleChange={handleChange}
                            handleBlur={handleBlur}
                            handleSubmit={handleSubmit}
                            isSubmitting={isSubmitting}
                        />
                    )}
                </Formik>
            );
        };
    };
};

export default withFormikEnhancement;

然后在表单组件中使用这个高阶组件:

import React from'react';
import withFormikEnhancement from './withFormikEnhancement';
import * as yup from 'yup';

const LoginForm = () => {
    return (
        <form>
            <input type="text" name="username" />
            {props.errors.username && props.touched.username && <span>{props.errors.username}</span>}
            <input type="password" name="password" />
            {props.errors.password && props.touched.password && <span>{props.errors.password}</span>}
            <button type="submit" disabled={props.isSubmitting}>登录</button>
        </form>
    );
};

const initialValues = {
    username: '',
    password: ''
};

const validationSchema = yup.object({
    username: yup.string().required('用户名必填'),
    password: yup.string().required('密码必填').min(6, '密码至少6位')
});

const EnhancedLoginForm = withFormikEnhancement(initialValues, validationSchema)(LoginForm);

const App = () => {
    return (
        <div>
            <EnhancedLoginForm />
        </div>
    );
};

export default App;

在这个例子中,withFormikEnhancement 高阶组件使用 Formik 为 LoginForm 组件提供了初始值、验证和提交功能,使得表单处理更加简洁和高效。