React 替代的支撑钻探方式(反向,从子级到父级)处理表单
P粉419164700
P粉419164700 2023-09-01 19:45:14
[React讨论组]

我是 React 新手,通过一些实践项目来学习它。我目前正在从事表单处理和验证工作。我在 SPA 中使用 React Router 的 Form 组件,并且在表单中我有 FormGroup 元素,它呈现标签输入和错误消息。我还在 FormGroup 组件中使用自己的输入组件来分离表单中使用的输入的逻辑和状态管理。

因此,我将 Form 组件和 FormGroup 组件放置在示例登录页面中,如下所示:

pages/Login.js

import { useState } from 'react';
import { Link, Form, useNavigate, useSubmit } from 'react-router-dom';

import FormGroup from '../components/UI/FormGroup';
import Button from '../components/UI/Button';
import Card from '../components/UI/Card';

import './Login.scss';

function LoginPage() {
    const navigate = useNavigate();
    const submit = useSubmit();
    const [isLoginValid, setIsLoginValid] = useState(false);
    const [isPasswordValid, setIsPasswordValid] = useState(false);
    var resetLoginInput = null;
    var resetPasswordInput = null;

    let isFormValid = false;

    if(isLoginValid && isPasswordValid) {
        isFormValid = true;
    }

    function formSubmitHandler(event) {
        event.preventDefault();

        if(!isFormValid) {
            return;
        }

        resetLoginInput();
        resetPasswordInput();

        submit(event.currentTarget);
    }

    function loginValidityChangeHandler(isValid) {
        setIsLoginValid(isValid);
    }

    function passwordValidityChangeHandler(isValid) {
        setIsPasswordValid(isValid);
    }

    function resetLoginInputHandler(reset) {
        resetLoginInput = reset;
    }

    function resetPasswordInputHandler(reset) {
        resetPasswordInput = reset;
    }

    function switchToSignupHandler() {
        navigate('/signup');
    }

    return (
        <div className="login">
            <div className="login__logo">
                Go Cup
            </div>
            <p className="login__description">
                Log in to your Go Cup account
            </p>
            <Card border>
                <Form onSubmit={formSubmitHandler}>
                    <FormGroup
                        id="login"
                        label="User name or e-mail address"
                        inputProps={{
                            type: "text",
                            name: "login",
                            validity: (value) => {
                                value = value.trim();
                                if(!value) {
                                    return [false, 'Username or e-mail address is required.']
                                } else if(value.length < 3 || value.length > 30) {
                                    return [false, 'Username or e-mail address must have at least 3 and at maximum 30 characters'];
                                } else {
                                    return [true, null];
                                }
                            },
                            onValidityChange: loginValidityChangeHandler,
                            onReset: resetLoginInputHandler
                        }}
                    />
                    <FormGroup
                        id="password"
                        label="Password"
                        sideLabelElement={
                            <Link to="/password-reset">
                                Forgot password?
                            </Link>
                        }
                        inputProps={{
                            type: "password",
                            name: "password",
                            validity: (value) => {
                                value = value.trim();
                                if(!value) {
                                    return [false, 'Password is required.']
                                } else if(value.length < 4 || value.length > 1024) {
                                    return [false, 'Password must be at least 4 or at maximum 1024 characters long.'];
                                } else {
                                    return [true, null];
                                }
                            },
                            onValidityChange: passwordValidityChangeHandler,
                            onReset: resetPasswordInputHandler
                        }}
                    />
                    <div className="text-center">
                        <Button className="w-100" type="submit">
                            Log in
                        </Button>
                        <span className="login__or">
                            or
                        </span>
                        <Button className="w-100" onClick={switchToSignupHandler}>
                            Sign up
                        </Button>
                    </div>
                </Form>
            </Card>
        </div>
    );
}

export default LoginPage;

正如您在上面的代码中看到的,我使用 FormGroup 组件并传递 onValidityChangeonReset 属性来获取 isValid 值的更新值。表单提交后重置输入的更改和重置函数等。使用我的自定义挂钩 useInput 在输入组件中创建 isValidreset 函数。我在值发生变化时传递 isValid 值,并使用 FormGroup 组件中定义的 props 从输入组件传递重置函数。我还在登录页面中使用 isLoginValidisPasswordValid states defiend 来存储从子输入组件传递的更新的 isValid 状态值。因此,我已经在输入组件中定义了状态,并使用 props 将它们传递给父组件,并将它们的值存储在该父组件中创建的其他状态中。正在进行的道具钻孔让我感觉有点不舒服。

状态是在输入组件内部管理的,我有这些状态:

  • 值:输入元素的值。
  • isInputTouched:确定用户是否已触摸/聚焦输入,以确定是否显示验证错误消息(如果有)。

我将一些函数(例如传递给输入组件的验证函数)组合并应用到这两个状态,以创建其他变量值来收集有关输入及其有效性的信息,例如该值是否有效(isValid)、是否有消息验证(消息),如果输入有效(isInputValid = isValid || !isInputTouched)来决定显示验证消息。

这些状态和值在我创建的自定义挂钩 useInput 中进行管理,如下所示:

hooks/use-state.js

import { useState, useCallback } from 'react';

function useInput(validityFn) {
    const [value, setValue] = useState('');
    const [isInputTouched, setIsInputTouched] = useState(false);

    const [isValid, message] = typeof validityFn === 'function' ? validityFn(value) : [true, null];
    const isInputValid = isValid || !isInputTouched;

    const inputChangeHandler = useCallback(event => {
        setValue(event.target.value);

        if(!isInputTouched) {
            setIsInputTouched(true);
        }
    }, [isInputTouched]);

    const inputBlurHandler = useCallback(() => {
        setIsInputTouched(true);
    }, []);

    const reset = useCallback(() => {
        setValue('');
        setIsInputTouched(false);
    }, []);

    return {
        value,
        isValid,
        isInputValid,
        message,
        inputChangeHandler,
        inputBlurHandler,
        reset
    };
}

export default useInput;

我目前在 Input.js 中使用这个自定义钩子,如下所示:

components/UI/Input.js

import { useEffect } from 'react';

import useInput from '../../hooks/use-input';

import './Input.scss';

function Input(props) {
    const {
        value,
        isValid,
        isInputValid,
        message,
        inputChangeHandler,
        inputBlurHandler,
        reset
    } = useInput(props.validity);

    const {
        onIsInputValidOrMessageChange,
        onValidityChange,
        onReset
    } = props;

    let className = 'form-control';

    if(!isInputValid) {
        className = `${className} form-control--invalid`;
    }

    if(props.className) {
        className = `${className} ${props.className}`;
    }

    useEffect(() => {
        if(onIsInputValidOrMessageChange && typeof onIsInputValidOrMessageChange === 'function') {
            onIsInputValidOrMessageChange(isInputValid, message);
        }
    }, [onIsInputValidOrMessageChange, isInputValid, message]);

    useEffect(() => {
        if(onValidityChange && typeof onValidityChange === 'function') {
            onValidityChange(isValid);
        }
    }, [onValidityChange, isValid]);

    useEffect(() => {
        if(onReset && typeof onReset === 'function') {
            onReset(reset);
        }
    }, [onReset, reset]);

    return (
        <input
            {...props}
            className={className}
            value={value}
            onChange={inputChangeHandler}
            onBlur={inputBlurHandler}
        />
    );
}

export default Input;

在输入组件中,我直接使用 isInputValid 状态将无效的 CSS 类添加到输入中。但我还将 isInputValidmessageisValid 状态和 reset 函数传递给父组件以在其中使用。为了传递这些状态和函数,我使用在 props 中定义的 onIsInputValidOrMessageChangeonValidityChangeonReset 函数(props 钻取但方向相反,从孩子到父母)。

这是 FormGroup 组件的定义以及我如何使用 FormGroup 内的输入状态来显示验证消息(如果有):

components/UI/FormGroup.js

import { useState } from 'react';

import Input from './Input';

import './FormGroup.scss';

function FormGroup(props) {
    const [message, setMessage] = useState(null);
    const [isInputValid, setIsInputValid] = useState(false);

    let className = 'form-group';

    if(props.className) {
        className = `form-group ${props.className}`;
    }

    let labelCmp = (
        <label htmlFor={props.id}>
            {props.label}
        </label>
    );

    if(props.sideLabelElement) {
        labelCmp = (
            <div className="form-label-group">
                {labelCmp}
                {props.sideLabelElement}
            </div>
        );
    }

    function isInputValidOrMessageChangeHandler(changedIsInputValid, changedMessage) {
        setIsInputValid(changedIsInputValid);
        setMessage(changedMessage);
    }

    return (
        <div className={className}>
            {labelCmp}
            <Input
                id={props.id}
                onIsInputValidOrMessageChange={isInputValidOrMessageChangeHandler}
                {...props.inputProps}
            />
            {!isInputValid && <p>{message}</p>}
        </div>
    );
}

export default FormGroup;

从上面的代码中可以看到,我定义了 messageisInputValid 状态来存储更新的 messageisInputValid code> 从输入组件传递的状态。我已经在输入组件中定义了 2 个状态来保存这些值,但我需要在此组件中定义另外 2 个状态来存储输入组件中更新和传递的值。这有点奇怪,对我来说似乎不是最好的方式。

问题是:我想我可以使用 React Context (useContext) 或 React Redux 来解决这里的 prop 钻取问题。但我不确定我当前的状态管理是否不好,是否可以使用 React Context 或 React Redux 来改善。因为根据我的了解,React Context 在状态频繁变化的情况下可能会很糟糕,但如果 Context 在应用程序范围内使用,那么这是有效的。在这里,我可以创建一个上下文来存储和更新整个表单,从而实现表单范围内的扩展。另一方面,React Redux 可能不是最适合的孤岛,并且可能有点矫枉过正。你们有什么感想?对于这种特定情况,什么可能是更好的替代方案?

注意:由于我是 React 的新手,因此我愿意接受您关于我所有编码的所有建议,从简单的错误到一般的错误。谢谢!

P粉419164700
P粉419164700

全部回复(2)
P粉627136450

关于 React 状态管理有两种主要思想流派:受控和非受控。受控表单可能会使用 React 上下文进行控制,其中可以在任何地方访问值以提供反应性。但是,受控输入可能会导致性能问题,尤其是在每个输入上更新整个表单时。这就是不受控制的表单出现的地方。通过这种范例,所有状态管理都必须利用浏览器的本机功能来显示状态。这种方法的主要问题是你失去了表单的 React 方面,你需要在提交时手动收集表单数据,并且为此维护多个引用可能很乏味。

受控输入如下所示:

const [name, setName] = useState("");

return <input value={name} onChange={(e) => setName(e.currentTarget.value)} />

编辑:正如@Arkellys指出的那样,您不一定需要引用来收集表单数据,这是一个使用 FormData

的示例

并且不受控制:

const name = useRef(null);
const onSubmit = () => {
    const nameContent = name.current.value;
}
return <input ref={name} defaultValue="" />

从这两个示例中可以明显看出,使用任一方法维护多组件表单都是乏味的,因此,通常使用库来帮助您管理表单。我个人推荐 React Hook Form 作为经过实战测试、维护良好且易于使用的表单图书馆。它采用不受控制的形式来实现最佳性能,同时仍然允许您观看单个输入以进行反应式渲染。

关于是否使用 Redux、React 上下文或任何其他状态管理系统,假设您正确实现的话,通常在性能方面没有什么区别。如果您喜欢 flux 架构,那么请务必使用 Redux,但在大多数情况下,React 上下文是既高性能又足够。

您的 useInput 自定义挂钩看起来是解决问题 react-hook-formreact-final-form 的勇敢但误导性的尝试代码>已经解决了。您正在创建不必要的复杂性和不可预测的副作用 有了这个抽象。此外,您镜像 props a> 这通常是 React 中的反模式。

如果您确实想实现自己的表单逻辑(我建议您不要这样做,除非是出于教育目的),您可以遵循以下准则:

  1. 在最高共同祖先处保留一个事实来源
  2. 避免镜像和复制状态
  3. 使用 useMemouseRef 尽可能少地重新渲染
P粉596191963

这是我用来在 Redux 等发布-订阅库和通过组件树传播状态之间做出决定的一个直接方面。

如果两个组件具有父子关系并且彼此距离最多两条边,则将子状态传播到父级

父级 -> child1-level1 -> child1-level2 ------ 好

父级 -> child1-level1 ------ 好

父级 -> child1-level1 -> child1-level2 -> child1-level3 --> 行程过多,无法将状态从 child1-level3 更改为父级

  • 如果交互组件之间的边距离超过 2 个,则使用 redux
  • 对同级组件使用 redux,即共享父组件且需要彼此通信的子组件(在侧面板中选择一个树项目,在主组件中显示所选项目的详细信息)

自实施以来

  • 我发现 useInput 是一种过度重构,您的输入组件应该足以管理与输入相关的操作,更好地抽象验证等方面
  • 您可以在表单提交时触发所有验证,在这种情况下,您不需要受控输入(在表单的 onSubmit 事件上附加验证)
  • 但是,如果您的表单包含太多字段(比如 >5),并且您想在提交之前验证该字段,您可以使用输入字段的 onBlur 事件,或者使用 onInput 以及 debounce 操作(例如来自 lodash 的 debounce 操作)或像这样实现

function debounce(func, timeout = 300){
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => { func.apply(this, args); }, timeout);
  };
}
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号