React 替代的支撑钻探方式(反向,从子级到父级)处理表单
<p>我是 React 新手,通过一些实践项目来学习它。我目前正在从事表单处理和验证工作。我在 SPA 中使用 React Router 的 Form 组件,并且在表单中我有 FormGroup 元素,它呈现标签输入和错误消息。我还在 FormGroup 组件中使用自己的输入组件来分离表单中使用的输入的逻辑和状态管理。</p>
<p>因此,我将 Form 组件和 FormGroup 组件放置在示例登录页面中,如下所示:</p>
<p><em>pages/Login.js</em></p>
<pre class="brush:js;toolbar:false;">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">
登录您的 Go Cup 帐户
</p>
<卡片边框>
<表单onSubmit={formSubmitHandler}>
<表格组
id=“登录”
label="用户名或电子邮件地址"
输入属性={{
类型:“文本”,
名称:“登录”,
有效性:(值)=> {
值 = value.trim();
如果(!值){
return [false, '需要用户名或电子邮件地址。']
} else if(value.length < 3 || value.length > 30) {
return [false, '用户名或电子邮件地址必须至少有 3 个字符,最多 30 个字符'];
} 别的 {
返回[真,空];
}
},
onValidityChange:loginValidityChangeHandler,
onReset:重置LoginInputHandler
}}
>>
<表格组
id=“密码”
标签=“密码”
sideLabelElement={
<链接到=“/密码重置”>
忘记密码?
链接>
}
输入属性={{
类型:“密码”,
名称:“密码”,
有效性:(值)=> {
值 = value.trim();
如果(!值){
return [false, '需要密码。']
} else if(value.length < 4 || value.length > 1024) {
return [false, '密码长度必须至少为 4 个或最多 1024 个字符。'];
} 别的 {
返回[真,空];
}
},
onValidityChange:passwordValidityChangeHandler,
onReset:重置密码输入处理程序
}}>>
<按钮类名=“w-100”类型=“提交”>
登录
</按钮>
或者
</span>
<按钮类名=“w-100” onClick={switchToSignupHandler}>
报名
</按钮>
</表格>
</卡>
;
);
}
export default LoginPage;
</pre>
<p>正如您在上面的代码中看到的,我使用 FormGroup 组件并传递 <code>onValidityChange</code> 和 <code>onReset</code> 属性来获取 <code>isValid</code> 值的更新值。表单提交后重置输入的更改和重置函数等。使用我的自定义挂钩 useInput 在输入组件中创建 <code>isValid</code> 和 <code>reset</code> 函数。我在值发生变化时传递 isValid 值,并使用 FormGroup 组件中定义的 props 从输入组件传递重置函数。我还在登录页面中使用 <code>isLoginValid</code> 和 <code>isPasswordValid</code> states defiend 来存储从子输入组件传递的更新的 <code>isValid</code> 状态值。因此,我已经在输入组件中定义了状态,并使用 props 将它们传递给父组件,并将它们的值存储在该父组件中创建的其他状态中。正在进行的道具钻孔让我感觉有点不舒服。</p>
<p>状态是在输入组件内部管理的,我有这些状态:</p>
<ul>
<li><strong>值:</strong>输入元素的值。</li>
<li><strong>isInputTouched</strong>:确定用户是否已触摸/聚焦输入,以确定是否显示验证错误消息(如果有)。</li>
</ul>
<p>我将一些函数(例如传递给输入组件的验证函数)组合并应用到这两个状态,以创建其他变量值来收集有关输入及其有效性的信息,例如该值是否有效(isValid)、是否有消息验证(消息),如果输入有效(<code>isInputValid = isValid || !isInputTouched</code>)来决定显示验证消息。</p>
<p>这些状态和值在我创建的自定义挂钩 <code>useInput</code> 中进行管理,如下所示:</p>
<p><em>hooks/use-state.js</em></p>
<pre class="brush:js;toolbar:false;">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;
</pre>
<p>我目前在 Input.js 中使用这个自定义钩子,如下所示:</p>
<p><em>components/UI/Input.js</em></p>
<pre class="brush:js;toolbar:false;">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;
</pre>
<p>在输入组件中,我直接使用 <code>isInputValid</code> 状态将无效的 CSS 类添加到输入中。但我还将 <code>isInputValid</code>、<code>message</code>、<code>isValid</code> 状态和 <code>reset</code> 函数传递给父组件以在其中使用。为了传递这些状态和函数,我使用在 props 中定义的 <code>onIsInputValidOrMessageChange</code>、<code>onValidityChange</code>、<code>onReset</code> 函数(props 钻取但方向相反,从孩子到父母)。</p>
<p>这是 FormGroup 组件的定义以及我如何使用 FormGroup 内的输入状态来显示验证消息(如果有):</p>
<p><em>components/UI/FormGroup.js</em></p>
<pre class="brush:js;toolbar:false;">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;
</pre>
<p>从上面的代码中可以看到,我定义了 <code>message</code> 和 <code>isInputValid</code> 状态来存储更新的 <code>message</code> 和 <code>isInputValid</code> code> 从输入组件传递的状态。我已经在输入组件中定义了 2 个状态来保存这些值,但我需要在此组件中定义另外 2 个状态来存储输入组件中更新和传递的值。这有点奇怪,对我来说似乎不是最好的方式。</p>
<p><strong>问题是:</strong>我想我可以使用 React Context (useContext) 或 React Redux 来解决这里的 prop 钻取问题。但我不确定我当前的状态管理是否不好,是否可以使用 React Context 或 React Redux 来改善。因为根据我的了解,React Context 在状态频繁变化的情况下可能会很糟糕,但如果 Context 在应用程序范围内使用,那么这是有效的。在这里,我可以创建一个上下文来存储和更新整个表单,从而实现表单范围内的扩展。另一方面,React Redux 可能不是最适合的孤岛,并且可能有点矫枉过正。你们有什么感想?对于这种特定情况,什么可能是更好的替代方案?</p>
<p><strong>注意:</strong>由于我是 React 的新手,因此我愿意接受您关于我所有编码的所有建议,从简单的错误到一般的错误。谢谢!</p>