React注册表单useRef获取用户输入值全为null如何解决
注册表单读取输入值全为null问题修复
问题复现
开发SignUp注册表单时,通过ref读取用户输入的表单值全部返回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;
问题原因
核心问题是组件传参结构和组件内部接收逻辑不匹配,附带两个容易引发异常的逻辑问题:
- 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。 - 提交逻辑中先调用
setIsSaving(true)触发状态更新,再读取ref值。React状态更新会触发组件重渲染,isSaving为true时表单模块会被卸载,极端情况下会导致ref被重置为null,读值失败。 - 附带细节错误:密码框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
相关产品推荐
相关产品推荐

