React登录表单拆分组件后useState变量按键时不更新
登录表单状态无法实时更新问题
原本通过FormInput组件映射实现登录表单,拆分字段类型为独立子组件后,credentials状态变量无法随用户输入实时更新,功能异常。
相关代码
Login组件代码
import React from 'react'; import FormInput from '../../components/Form Input/FormInput'; import { loginInputs } from '../../formSource/formSourceData'; import './Login.scss'; import { useState, useContext } from 'react'; import { useNavigate, Link, useLocation } from 'react-router-dom'; import { useToastContext } from '../../context/toastContext'; import useFetch from '../../Hooks/UseFetch'; import { AuthContext } from '../../context/AuthContext' import axios from 'axios' const Login = () => { const [credentials, setCredentials] = useState({ email: undefined, password: undefined, }); const { user, loading, error, dispatch} = useContext(AuthContext) const { toastDispatch } = useToastContext(); const navigate = useNavigate(); const { state } = useLocation() const handleChange = (e) => { console.log(e.target.name) setCredentials({ ...credentials, [e.target.name]: e.target.value }); console.log(credentials) }; const handleSubmit = async (e) => { e.preventDefault() dispatch({type: "LOGIN_START"}) try{ const res = await axios.post("/api/auth/login", credentials) dispatch({type:"LOGIN_SUCCESS", payload: res.data}) navigate(state?.path || '/auth/teacher/dashboard'); } catch(err) { dispatch({type: "LOGIN_FAILURE", payload: err.response.data}) } } return ( <div className="container"> <div className="formWrapper"> <h1>Login</h1> <form className="loginForm" > {loginInputs.map((input) => ( <FormInput key={input.id} {...input} handleChange={handleChange} /> ))} <button type="submit" className="loginButton" onClick={handleSubmit}> Login </button> </form> <p className="forgotPassword"> <Link to="/forgot-password">Forgot Password</Link> </p> <p className="accountText"> Not signed up? <Link to="/register">Register</Link> </p> </div> </div> ); }; export default Login;
修改后的FormInput组件代码
import { InputRounded } from '@mui/icons-material'; import React, { useState } from 'react'; import './formInput.scss'; const FormInput = (props) => { const { label, type, errorMessage, handleChange, id, value, ...inputProps } = props; const [focused, setFocused] = useState(false); const [passwordShown, setPasswordShown] = useState(false); const handleFocus = (e) => { setFocused(true); }; const togglePassword = () => { setPasswordShown(!passwordShown); }; const Dropdown = () => { return ( <select className="formElementInput" value={value} name={inputProps.name} onChange={handleChange} > <option className="default" selected disabled> {inputProps.placeholder} </option> {inputProps.options.map((option) => ( <option className="option" value={option}> {option} </option> ))} </select> ); }; const Input = () => { return ( <div className="formGroup"> <input className="formElementInput" value={value} name={props.name} placeholder={props.placeholder} type={passwordShown ? 'text' : type} onChange={props.handleChange} onBlur={handleFocus} focused={focused.toString()} onFocus={() => inputProps.name === 'confirmPassword' && setFocused(true) } /> <span className="icon" onClick={togglePassword}> {passwordShown ? inputProps.icon : inputProps.opposite} </span> </div> ); }; return ( <div className="formElement"> <label className="formElementLabel">{label}</label> {type === 'dropdown' ? ( <Dropdown /> ) : ( <Input/> )} <span className="errorMessage">{errorMessage}</span> </div> ); }; export default FormInput;
登录字段配置
export const loginInputs = [ { id: 1, name: "email", type: "email", placeholder: "Email", label: "Email", errorMessage: "Enter a valid email address", required: true }, { id: 2, name: "password", type: "password", placeholder: "Password", label: "Password", errorMessage: "A password should be more than 8 characters.", required: true, icon: <Visibility/>, opposite: <VisibilityOff/> } ]
原始可正常工作的FormInput组件代码
import { InputRounded } from '@mui/icons-material'; import React, { useState } from 'react'; import './formInput.scss'; const FormInput = (props) => { const { label, type, errorMessage, handleChange, id, value, ...inputProps } = props; const [focused, setFocused] = useState(false); const [passwordShown, setPasswordShown] = useState(false); const handleFocus = (e) => { setFocused(true); }; const togglePassword = () => { setPasswordShown(!passwordShown); }; const Dropdown = () => { return ( <select className="formElementInput" value={value} name={inputProps.name} onChange={handleChange} > <option className="default" selected disabled> {inputProps.placeholder} </option> {inputProps.options.map((option) => ( <option className="option" value={option}> {option} </option> ))} </select> ); }; const Input = () => { return ( <div className="formGroup"> <input className="formElementInput" value={value} name={props.name} placeholder={props.placeholder} type={passwordShown ? 'text' : type} onChange={props.handleChange} onBlur={handleFocus} focused={focused.toString()} onFocus={() => inputProps.name === 'confirmPassword' && setFocused(true) } /> <span className="icon" onClick={togglePassword}> {passwordShown ? inputProps.icon : inputProps.opposite} </span> </div> ); }; return ( <div className="formElement"> <label className="formElementLabel">{label}</label> {type === 'dropdown' ? ( <select className="formElementInput" value={value} name={inputProps.name} onChange={handleChange} > <option className="default" selected disabled> {inputProps.placeholder} </option> {inputProps.options.map((option) => ( <option className="option" value={option} > {option} </option> ))} </select> ) : ( <div className="formGroup"> <input className="formElementInput" value={value} name={inputProps.name} placeholder={inputProps.placeholder} type={passwordShown ? "text" : type} onChange={handleChange} onBlur={handleFocus} focused={focused.toString()} onFocus={() => inputProps.name === 'confirmPassword' && setFocused(true) } /> <span className="icon" onClick={togglePassword}> {passwordShown ? inputProps.icon : inputProps.opposite} </span> </div> )} <span className="errorMessage">{errorMessage}</span> </div> ); }; export default FormInput;
问题分析
- 受控组件未绑定状态值:Login组件中未将
credentials的对应字段值传递给FormInput,导致input组件处于非受控状态,React无法同步输入内容与状态。 - 状态更新的闭包问题:
handleChange中直接使用当前credentials对象更新状态,因useState异步更新特性,可能导致状态更新不及时。 - 组件属性不一致:修改后的FormInput组件中,Input的
name属性使用props.name,而Dropdown使用inputProps.name,虽逻辑上一致,但易引发混淆;同时Input的onChange调用props.handleChange,与解构出的handleChange变量冗余,增加出错概率。
解决方案
1. 绑定受控组件的value值
在Login组件的FormInput映射中,添加对应字段的value属性,将input转为受控组件:
{loginInputs.map((input) => ( <FormInput key={input.id} {...input} value={credentials[input.name]} // 新增这一行 handleChange={handleChange} /> ))}
2. 使用函数式更新状态
修改handleChange为函数式更新,避免闭包导致的状态不一致:
const handleChange = (e) => { const { name, value } = e.target; setCredentials(prevState => ({ ...prevState, [name]: value })); };
3. 统一FormInput组件的属性引用
修改FormInput中Input组件的属性,统一使用解构后的变量,保持代码一致性:
const Input = () => { return ( <div className="formGroup"> <input className="formElementInput" value={value} name={inputProps.name} // 改为inputProps.name,与Dropdown保持一致 placeholder={inputProps.placeholder} // 改为inputProps.placeholder type={passwordShown ? 'text' : type} onChange={handleChange} // 改为解构后的handleChange,替代props.handleChange onBlur={handleFocus} focused={focused.toString()} onFocus={() => inputProps.name === 'confirmPassword' && setFocused(true) } /> <span className="icon" onClick={togglePassword}> {passwordShown ? inputProps.icon : inputProps.opposite} </span> </div> ); };
完成以上修改后,credentials状态即可随用户输入实时更新,表单恢复正常功能。
内容的提问来源于stack exchange,提问作者lmcc
相关产品推荐
相关产品推荐

