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

React注册表单useRef获取用户输入值全为null如何解决

注册表单读取输入值全为null问题修复

问题复现

开发SignUp注册表单时,通过ref读取用户输入的表单值全部返回null,效果如下:
表单读取值全为null效果

涉及两个组件的代码如下:

Signup.js

import React, { useContext, useState, useRef } from "react";
import Modal from "../UI/Modal";
import classes from "./Login.module.css";
import Input from "../UI/Input/Input";

const Signup = (props) => {
  const firstnameInputRef = useRef();
  const lastnameInputRef = useRef();
  const emailInputRef = useRef();
  const passwordInputRef = useRef();

  const [isCanceling, setIsCanceling] = useState(false);
  const [isSaving, setIsSaving] = useState(false);
  const [didSave, setDidSave] = useState(false);
  const [isErrorOnSave, setIsErrorOnSave] = useState(false);

  const errorOnSignupHandler = () => {
    setIsErrorOnSave(true);
  };

  const signupHandler = async (clientData) => {
    setIsSaving(true);
    const enteredFirstname = firstnameInputRef.current.value;
    const enteredLastname = lastnameInputRef.current.value;
    const enteredEmail = emailInputRef.current.value;
    const enteredPassword = passwordInputRef.current.value;

    const newClientData = {
      firstname: enteredFirstname,
      lastname: enteredLastname,
      email: enteredEmail,
      password: enteredPassword,
    };

    console.log(newClientData);

    const response = await fetch("http://localhost:3000/clients", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(newClientData),
    });

    if (!response.ok) {
      errorOnSignupHandler();
    } else {
      setIsSaving(false);
      setIsCanceling(false);
      setDidSave(true);
    }
  };

  const isSavingModalContent = <p>Saving new user...</p>;

  const errorOnSavingModalContent = (
    <React.Fragment>
      <p>The user account could not be created. Please try again later</p>
      <div className={classes.actions}>
        <button className={classes.button} onClick={props.onClose}>
          Close
        </button>
      </div>
    </React.Fragment>
  );

  const didSaveModalContent = (
    <React.Fragment>
      <p>User account created, welcome!</p>
      <div className={classes.actions}>
        <button className={classes.button} onClick={props.onClose}>
          Close
        </button>
      </div>
    </React.Fragment>
  );

  const SignupButtons = (
    <React.Fragment>
      <button className={classes["button--alt"]} onClick={signupHandler}>
        Sign-Up
      </button>
      <button className={classes["button--alt"]} onClick={props.onClose}>
        Close
      </button>
    </React.Fragment>
  );

  const modalActions = (
    <div className={classes.actions}>{!isCanceling ? SignupButtons : ""}</div>
  );

  const SignupModalContent = (
    <React.Fragment>
      <Input
        ref={firstnameInputRef}
        id="firstname"
        label="First Name"
        type="text"
      />
      <Input
        ref={lastnameInputRef}
        id="lastname"
        label="Last Name"
        type="text"
      />
      <Input
        ref={emailInputRef}
        id="email"
        label="E-Mail"
        type="email"
        autodata="off"
      />
      <Input
        ref={passwordInputRef}
        id="paswword"
        label="Password"
        type="password"
        autodata="new-password"
      />
      <Input
        id="paswword2"
        label="Confirm-Password"
        type="password"
        autodata="new-password"
      />
      {modalActions}
    </React.Fragment>
  );

  return (
    <Modal onClose={props.onClose}>
      {!isCanceling && !isSaving && !isErrorOnSave && !didSave && SignupModalContent}
      {isSaving && isSavingModalContent}
      {isErrorOnSave && errorOnSavingModalContent}
      {!isSaving && didSave && didSaveModalContent}
    </Modal>
  );
};

export default Signup;

Input.js

import React from 'react';
import classes from './Input.module.css';

const Input = React.forwardRef((props, ref) => {
    return(
        <div className={classes.input}>
            <label htmlFor={props.input.id}>{props.label}</label>
            <input ref={ref} {...props.input}/>
        </div>
    );
});

export default Input;

问题原因

核心问题是组件传参结构和组件内部接收逻辑不匹配,附带两个容易引发异常的逻辑问题:

  1. Input组件内部约定,所有原生input标签的属性(id、type等)需要放在input这个prop对象下传入,组件内部通过props.input.id绑定label、通过{...props.input}给原生input传属性。但实际调用Input时,id、type等属性直接平铺在组件props顶层,没有放入input对象,导致props.input为undefined,访问props.input.id直接触发JS运行错误,组件渲染异常,ref无法正确绑定到原生input标签,读取时ref.current为null。
  2. 提交逻辑中先调用setIsSaving(true)触发状态更新,再读取ref值。React状态更新会触发组件重渲染,isSaving为true时表单模块会被卸载,极端情况下会导致ref被重置为null,读值失败。
  3. 附带细节错误:密码框id拼写错误(paswword应为password)、自动填充属性名写错(autodata应为原生的autoComplete)、确认密码框未绑定ref无法做一致性校验。

修复方案

选择以下任意一种方式修复传参结构问题,再调整提交逻辑顺序即可:

方式一:调整Signup组件传参结构(兼容现有Input组件写法)

所有Input调用处,把原生input相关属性统一放到input字段下,同时修正拼写错误:

const SignupModalContent = (
  <React.Fragment>
    <Input
      ref={firstnameInputRef}
      label="First Name"
      input={{
        id: "firstname",
        type: "text",
      }}
    />
    <Input
      ref={lastnameInputRef}
      label="Last Name"
      input={{
        id: "lastname",
        type: "text",
      }}
    />
    <Input
      ref={emailInputRef}
      label="E-Mail"
      input={{
        id: "email",
        type: "email",
        autoComplete: "off",
      }}
    />
    <Input
      ref={passwordInputRef}
      label="Password"
      input={{
        id: "password",
        type: "password",
        autoComplete: "new-password",
      }}
    />
    {/* 确认密码框需要校验的话记得新增ref绑定 */}
    <Input
      id="password2"
      label="Confirm-Password"
      input={{
        type: "password",
        autoComplete: "new-password",
      }}
    />
    {modalActions}
  </React.Fragment>
);

同时调整signupHandler的执行顺序,先读取输入值再更新加载状态:

const signupHandler = async (clientData) => {
  // 先读取所有输入值
  const enteredFirstname = firstnameInputRef.current.value;
  const enteredLastname = lastnameInputRef.current.value;
  const enteredEmail = emailInputRef.current.value;
  const enteredPassword = passwordInputRef.current.value;

  const newClientData = {
    firstname: enteredFirstname,
    lastname: enteredLastname,
    email: enteredEmail,
    password: enteredPassword,
  };
  console.log(newClientData);

  // 值读取完成后再更新加载状态,避免表单卸载导致ref失效
  setIsSaving(true);

  // 后续fetch请求逻辑保持不变
  const response = await fetch("http://localhost:3000/clients", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(newClientData),
  });
  // 后续响应处理逻辑不变
};

方式二:修改Input组件逻辑(兼容现有Signup传参写法)

如果不想调整传参结构,可以修改Input组件的属性接收逻辑,直接平铺接收props:

import React from 'react';
import classes from './Input.module.css';

const Input = React.forwardRef((props, ref) => {
    // 提取非原生属性,剩余属性透传给原生input
    const { label, id, type, autoComplete, ...restProps } = props;
    return(
        <div className={classes.input}>
            <label htmlFor={id}>{label}</label>
            <input 
              ref={ref} 
              id={id} 
              type={type} 
              autoComplete={autoComplete}
              {...restProps}
            />
        </div>
    );
});

export default Input;

修改后只要把Signup里Input组件的autodata属性改成autoComplete、修正密码框id拼写,再调整signupHandler的执行顺序即可正常读值。


内容的提问来源于stack exchange,提问作者htamayo

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 17:51:24