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
相关产品推荐
相关产品推荐

