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

React中条件包裹Input组件且避免失焦的安全实现方案

解决React条件包裹Input导致重渲染失焦的问题

我之前通过判断error是否存在,决定用div包裹Input组件和错误信息,但重渲染时Input会失去焦点。需求是仅在有error时用div包裹Input,无error时不包裹,求可行的解决方法。

原无条件包裹代码

import * as React from "react"

import { cn } from "@/lib/utils"
import { FieldError } from "react-hook-form";
import ErrorText from "../shared/ErrorText";

export interface InputProps
  extends React.InputHTMLAttributes<HTMLInputElement> {
  error?: FieldError | undefined;
  errorClassName?: string;
}

const Input = React.forwardRef<HTMLInputElement, InputProps>(
  ({ className, type, error, errorClassName, ...props }, ref) => {
    return <div className="flex flex-col">
      <input
        type={type}
        className={cn(
          "flex h-12 w-full rounded-xl border border-input bg-background px-3 py-2 text-md file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
          className
        )}
        ref={ref}
        {...props}
      />
      {error && <ErrorText className={errorClassName}>{error.message}</ErrorText>}
    </div>
  }
)
Input.displayName = "Input"

export { Input }

原条件包裹(会失焦)代码

export interface InputProps
  extends React.InputHTMLAttributes<HTMLInputElement> {
  error?: FieldError | undefined;
  errorClassName?: string;
}

const Input = React.forwardRef<HTMLInputElement, InputProps>(
  ({ className, type, error, errorClassName, ...props }, ref) => {
    const child = (
      <>
        <input
          type={type}
          className={cn(
            "flex h-12 w-full rounded-xl border border-input bg-background px-3 py-2 text-md file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
            className
          )}
          ref={ref}
          {...props}
        />
      </>
    )

    if (error) {
      return (
        <div className="flex flex-col gap-1">
          {child}
          {<ErrorText className={errorClassName}>{error.message}</ErrorText>}
        </div>
      )
    }
    return child;
  }
)
Input.displayName = "Input"

export { Input }

核心原因

React重渲染时,如果组件的DOM结构发生变化(比如从Fragment直接返回input,变成用div包裹),React会卸载原有的input元素并重新创建,导致焦点丢失。

可行解决方法

方法1:始终保留外层容器(推荐)

不用条件切换外层元素,而是始终用div包裹,通过条件控制容器的样式和错误信息的显示。这样DOM结构保持稳定,input不会被卸载,自然不会失焦。

修改后的代码:

import * as React from "react"

import { cn } from "@/lib/utils"
import { FieldError } from "react-hook-form";
import ErrorText from "../shared/ErrorText";

export interface InputProps
  extends React.InputHTMLAttributes<HTMLInputElement> {
  error?: FieldError | undefined;
  errorClassName?: string;
}

const Input = React.forwardRef<HTMLInputElement, InputProps>(
  ({ className, type, error, errorClassName, ...props }, ref) => {
    return (
      <div className={cn("flex flex-col", error && "gap-1")}>
        <input
          type={type}
          className={cn(
            "flex h-12 w-full rounded-xl border border-input bg-background px-3 py-2 text-md file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
            className
          )}
          ref={ref}
          {...props}
        />
        {error && <ErrorText className={errorClassName}>{error.message}</ErrorText>}
      </div>
    )
  }
)
Input.displayName = "Input"

export { Input }

说明:

  • 始终用div作为外层容器,通过cn工具类条件添加gap-1样式(仅当error存在时)
  • 错误信息依然保持条件渲染,不影响DOM结构的稳定性
  • 这种方式最简洁,完全符合React的渲染机制,不会触发input的卸载重建

方法2:给外层元素添加稳定的key(备选)

如果一定要切换外层元素类型(比如无error时返回Fragment,有error时返回div),可以给外层元素添加固定的key,让React识别出这是同一个组件实例,避免卸载input:

const Input = React.forwardRef<HTMLInputElement, InputProps>(
  ({ className, type, error, errorClassName, ...props }, ref) => {
    const inputElement = (
      <input
        type={type}
        className={cn(
          "flex h-12 w-full rounded-xl border border-input bg-background px-3 py-2 text-md file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
          className
        )}
        ref={ref}
        {...props}
      />
    )

    if (error) {
      return (
        <div key="input-container" className="flex flex-col gap-1">
          {inputElement}
          <ErrorText className={errorClassName}>{error.message}</ErrorText>
        </div>
      )
    }
    return <React.Fragment key="input-container">{inputElement}</React.Fragment>;
  }
)

说明:

  • 给外层的div和Fragment添加相同的key="input-container",React会认为这是同一个位置的元素,不会卸载内部的input
  • 这种方式不如方法1直观,除非有特殊需求不希望无error时存在div标签,否则优先选方法1

方法3:手动保存并恢复焦点(hack方式)

如果以上两种方法都不适用,可以手动保存input的焦点状态,在重渲染后恢复:

const Input = React.forwardRef<HTMLInputElement, InputProps>(
  ({ className, type, error, errorClassName, ...props }, ref) => {
    const [isFocused, setIsFocused] = React.useState(false);
    const inputRef = React.useRef<HTMLInputElement>(null);

    // 同步外部ref
    React.useImperativeHandle(ref, () => inputRef.current!);

    const handleFocus = () => setIsFocused(true);
    const handleBlur = () => setIsFocused(false);

    // 重渲染后恢复焦点
    React.useEffect(() => {
      if (isFocused && inputRef.current) {
        inputRef.current.focus();
      }
    }, [error, isFocused]);

    const inputElement = (
      <input
        type={type}
        className={cn(
          "flex h-12 w-full rounded-xl border border-input bg-background px-3 py-2 text-md file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
          className
        )}
        ref={inputRef}
        onFocus={handleFocus}
        onBlur={handleBlur}
        {...props}
      />
    )

    if (error) {
      return (
        <div className="flex flex-col gap-1">
          {inputElement}
          <ErrorText className={errorClassName}>{error.message}</ErrorText>
        </div>
      )
    }
    return inputElement;
  }
)

说明:

  • 通过useState记录input是否处于聚焦状态
  • 在useEffect中监听error变化,当重渲染后如果之前是聚焦状态,手动调用focus()恢复
  • 这种方式需要额外的状态管理,代码复杂度高,仅作为极端场景下的备选

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 13:42:15