React表单按钮禁用状态异常:输入框退格重输入时逻辑反转
登录表单按钮状态反转问题排查与修复
问题描述
使用React的useReducer和useState实现基础登录表单,包含邮箱、密码输入框和提交按钮。测试时发现:输入含@的有效邮箱后删除@,再重新输入@,按钮启用状态出现反转——邮箱含@时按钮本应启用却被禁用,不含@时本应禁用却启用。该问题仅在输入后退格再重新输入时触发,正常顺序填写功能正常。
问题根源
核心原因是**useReducer的状态更新是异步的**:在emailChangeHandler或passwordChangeHandler中,调用dispatch后立刻读取emailState或passwordState,此时拿到的是更新前的旧状态值,导致formIsValid的计算基于过期状态,最终出现反转错误。
例如:
- 输入有效邮箱后,
emailState.isValid为true - 删除@,调用
dispatchEmail将isValid改为false,但此时立刻执行setFormIsValid(emailState.isValid && ...),emailState.isValid还是旧值true,导致formIsValid被错误设置为true - 重新输入@,调用
dispatchEmail将isValid改为true,但此时读取的emailState.isValid还是旧值false,导致formIsValid被错误设置为false,最终按钮状态反转
修复方案
有两种可靠的修复方式:
方式1:使用useEffect监听状态变化更新表单有效性
利用useEffect监听emailState.isValid和passwordState.isValid的变化,当这两个状态更新后,再计算并设置formIsValid,确保使用的是最新状态值。
方式2:直接使用最新输入值计算有效性
在输入变化时,直接用当前输入的最新值(event.target.value)计算邮箱/密码的有效性,结合另一个输入框的当前状态值,直接设置formIsValid,绕开异步状态更新的问题。
完整修复代码示例(方式1)
import React, { useState, useReducer, useEffect } from 'react'; import Card from '../UI/Card/Card'; import classes from './Login.module.css'; import Button from '../UI/Button/Button'; const emailReducer = (state, action) => { if(action.type === 'USER_INPUT') { return {value: action.val, isValid: action.val.includes('@') }; } if (action.type === 'INPUT_BLUR') { return {value: state.value, isValid: state.value.includes('@') }; } return { value: '', isValid: false } }; const passwordReducer = (state, action) => { if(action.type === 'USER_INPUT') { return {value: action.val, isValid: action.val.trim().length > 6} } if(action.type === 'INPUT_BLUR') { return {value: state.value, isValid: state.value.trim().length > 6} } }; const Login = (props) => { const [formIsValid, setFormIsValid] = useState(false); const [emailState, dispatchEmail] = useReducer(emailReducer, {value: '', isValid: null}); const [passwordState, dispatchPassword] = useReducer(passwordReducer, {value: '', isValid: null}) // 监听邮箱和密码状态变化,更新表单有效性 useEffect(() => { setFormIsValid(emailState.isValid && passwordState.isValid); }, [emailState.isValid, passwordState.isValid]); const emailChangeHandler = (event) => { dispatchEmail({type: 'USER_INPUT', val: event.target.value}); }; const passwordChangeHandler = (event) => { dispatchPassword({type: 'USER_INPUT', val: event.target.value}); }; const validateEmailHandler = () => { dispatchEmail({type: 'INPUT_BLUR'}); }; const validatePasswordHandler = () => { dispatchPassword({type: 'INPUT_BLUR'}) }; const submitHandler = (event) => { event.preventDefault(); props.onLogin(emailState.value, passwordState.value); }; return ( <Card className={classes.login}> <form onSubmit={submitHandler}> <div className={`${classes.control} ${emailState.isValid === false ? classes.invalid : '' }`} > <label htmlFor="email">E-Mail</label> <input type="email" id="email" value={emailState.value} onChange={emailChangeHandler} onBlur={validateEmailHandler} /> </div> <div className={`${classes.control} ${passwordState.isValid === false ? classes.invalid : '' }`} > <label htmlFor="password">Password</label> <input type="password" id="password" value={passwordState.value} onChange={passwordChangeHandler} onBlur={validatePasswordHandler} /> </div> <div className={classes.actions}> <Button type="submit" className={classes.btn} disabled={!formIsValid}> Login </Button> </div> </form> </Card> ); }; export default Login;
完整修复代码示例(方式2)
import React, { useState, useReducer } from 'react'; import Card from '../UI/Card/Card'; import classes from './Login.module.css'; import Button from '../UI/Button/Button'; const emailReducer = (state, action) => { if(action.type === 'USER_INPUT') { return {value: action.val, isValid: action.val.includes('@') }; } if (action.type === 'INPUT_BLUR') { return {value: state.value, isValid: state.value.includes('@') }; } return { value: '', isValid: false } }; const passwordReducer = (state, action) => { if(action.type === 'USER_INPUT') { return {value: action.val, isValid: action.val.trim().length > 6} } if(action.type === 'INPUT_BLUR') { return {value: state.value, isValid: state.value.trim().length > 6} } }; const Login = (props) => { const [formIsValid, setFormIsValid] = useState(false); const [emailState, dispatchEmail] = useReducer(emailReducer, {value: '', isValid: null}); const [passwordState, dispatchPassword] = useReducer(passwordReducer, {value: '', isValid: null}) const emailChangeHandler = (event) => { const newEmail = event.target.value; const newEmailIsValid = newEmail.includes('@'); dispatchEmail({type: 'USER_INPUT', val: newEmail}); // 直接用最新的邮箱有效性值计算表单状态 setFormIsValid(newEmailIsValid && passwordState.isValid); }; const passwordChangeHandler = (event) => { const newPassword = event.target.value; const newPasswordIsValid = newPassword.trim().length > 6; dispatchPassword({type: 'USER_INPUT', val: newPassword}); // 直接用最新的密码有效性值计算表单状态 setFormIsValid(newPasswordIsValid && emailState.isValid); }; const validateEmailHandler = () => { dispatchEmail({type: 'INPUT_BLUR'}); }; const validatePasswordHandler = () => { dispatchPassword({type: 'INPUT_BLUR'}) }; const submitHandler = (event) => { event.preventDefault(); props.onLogin(emailState.value, passwordState.value); }; return ( <Card className={classes.login}> <form onSubmit={submitHandler}> <div className={`${classes.control} ${emailState.isValid === false ? classes.invalid : '' }`} > <label htmlFor="email">E-Mail</label> <input type="email" id="email" value={emailState.value} onChange={emailChangeHandler} onBlur={validateEmailHandler} /> </div> <div className={`${classes.control} ${passwordState.isValid === false ? classes.invalid : '' }`} > <label htmlFor="password">Password</label> <input type="password" id="password" value={passwordState.value} onChange={passwordChangeHandler} onBlur={validatePasswordHandler} /> </div> <div className={classes.actions}> <Button type="submit" className={classes.btn} disabled={!formIsValid}> Login </Button> </div> </form> </Card> ); }; export default Login;
内容的提问来源于stack exchange,提问作者Nate1zn
相关产品推荐
相关产品推荐

