You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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;

问题分析

  1. 受控组件未绑定状态值:Login组件中未将credentials的对应字段值传递给FormInput,导致input组件处于非受控状态,React无法同步输入内容与状态。
  2. 状态更新的闭包问题:handleChange中直接使用当前credentials对象更新状态,因useState异步更新特性,可能导致状态更新不及时。
  3. 组件属性不一致:修改后的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 07:40:33