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

React输入框绑定onChange后每输入一个字符就重渲染失焦如何解决

问题修复方案

核心原因

你把Emailform、Passform、AuthType、Login、Signup这些子组件直接定义在了Auth组件内部,每次输入内容触发setEmail/setPassword更新Auth组件状态时,这些内部子组件都会被重新创建,React会判定为全新组件,直接卸载旧实例挂载新实例,自然就会导致输入框失焦,只能输入单个字符。
另外你还有两个隐性问题:

  • useClickOutside钩子的useEffect没有加依赖数组,每次组件渲染都会重复绑定、解绑事件监听器
  • 自定义Form组件内读取属性错误,你传递的是type属性,组件内错误读取了props.input

修复步骤

  1. 将所有定义在Auth组件内部的子组件,全部移到Auth组件外部,组件需要用到的状态、方法通过props传递
  2. 给useClickOutside的useEffect添加空依赖数组,保证事件只在组件挂载时绑定一次
  3. 修正Form组件的属性读取逻辑
  4. 移除输入框外不必要的form标签,避免触发默认提交刷新页面的行为

修改后的代码示例

import style from "./auth.module.css";
import { useEffect, useRef, useState } from "react";
import { useAuthState } from 'react-firebase-hooks/auth';
import { auth, signInWithEmailAndPassword, signInWithGoogle, registerWithEmailAndPassword } from "../../firebase";
import { CSSTransition } from "react-transition-group";

// 移到Auth外部的子组件
const Emailform = ({email, setEmail}) => {
  return (
    <div className={style.formBox}>
      <label className={style.label}>邮箱:</label>
      <div className={style.input}>
        <input 
          type="email" 
          name="email" 
          required
          value={email} 
          onChange={(e) => setEmail(e.target.value)} 
        />
      </div>
    </div>
  );
}

const Passform = ({password, setPassword}) => {
  return (
    <div className={style.formBox}>
      <label className={style.label}>密码:</label>
      <div className={style.input}>
        <input 
          type="password" 
          name="password" 
          required
          value={password} 
          onChange={(e) => setPassword(e.target.value)} 
        />
      </div>
    </div>
  );
}

const AuthType = ({goToMenu, setActiveMenu, link, children}) => {
  return (
    <a
      href={link || '#'}
      className={style.menuItem}
      onClick={(e) => {
        e.preventDefault()
        goToMenu && setActiveMenu(goToMenu)
      }}
    >
      {children}
    </a>
  );
}

const Login = ({activeMenu, domNode, email, setEmail, password, setPassword, setActiveMenu, signInWithEmailAndPassword, signInWithGoogle}) => {
  return (
    <CSSTransition in={activeMenu === "main"} unmountOnExit timeout={500}>
      <div ref={domNode}>
        <div className={style.login}>
          <h1 className={style.title}>Clip It</h1>
          <Emailform email={email} setEmail={setEmail}/>
          <Passform password={password} setPassword={setPassword}/>
          <div className={style.button}>
            <button 
              type="button" 
              onClick={() => signInWithEmailAndPassword(email, password)}
            >
              登录
            </button>
            <button 
              type="button" 
              onClick={signInWithGoogle}
            >
              谷歌账号登录
            </button>
          </div>
          <div className={style.text}>
            <p className={style.plink}>忘记密码</p>
            <div>
              没有账号?&nbsp;
              <AuthType goToMenu="Signup" setActiveMenu={setActiveMenu}>点击注册</AuthType>
            </div>
          </div>
        </div>
      </div>
    </CSSTransition>
  );
}

const Signup = ({activeMenu, domNode, setActiveMenu}) => {
  return (
    <CSSTransition in={activeMenu === "Signup"} unmountOnExit timeout={500}>
      <div ref={domNode}>
        <div className={style.signUp}>
          <div className={style.title}> Clip It</div>
          <Form label="名字" type="text" />
          <Form label="姓氏" type="text" />
          <Form label="邮箱" type="email"/>
          <Form label="出生日期" type="date" />
          <Form label="密码" type="password"/>
          <Form label="确认密码" type="password" />
          <div className={style.button}>
            <button type="button">注册</button>
          </div>
          <div className={style.text}>
            已有账号?&nbsp;
            <AuthType goToMenu="main" setActiveMenu={setActiveMenu}>立即登录</AuthType>
          </div>
        </div>
      </div>
    </CSSTransition>
  );
}

const Form = (props) => {
  return (
    <div className={style.formBox}>
      <label className={style.label}>{props.label}:</label>
      <div className={style.input}>
        <input 
          type={props.type} 
          name={props.type} 
          required
        />
      </div>
    </div>
  );
}

// 修复useClickOutside的依赖
const useClickOutside = (handler) => {
  let domNode = useRef();
  useEffect(() => {
    let clickListener = (event) => {
      if (domNode.current && !domNode.current.contains(event.target)) {
        handler();
      }
    };
    document.addEventListener("mousedown", clickListener);
    return () => {
      document.removeEventListener("mousedown", clickListener);
    };
  }, []); // 添加空依赖避免重复绑定
  return domNode;
};

function Auth() {
  const [activeMenu, setActiveMenu] = useState("main");
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [user, loading, error] = useAuthState(auth);

  const domNode = useClickOutside(() => {
    setActiveMenu("main");
  });

  return (
    <div className={style.container}>
      <Login 
        activeMenu={activeMenu}
        domNode={domNode}
        email={email}
        setEmail={setEmail}
        password={password}
        setPassword={setPassword}
        setActiveMenu={setActiveMenu}
        signInWithEmailAndPassword={signInWithEmailAndPassword}
        signInWithGoogle={signInWithGoogle}
      />
      <Signup 
        activeMenu={activeMenu}
        domNode={domNode}
        setActiveMenu={setActiveMenu}
      />
    </div>
  );
}

export default Auth;

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.30 13:54:03