React+TypeScript使用forwardRef报错:Property 'current'不存在
问题:如何在forwardRef封装的FormInput组件中判断输入框是否处于激活状态?
我开发了一个接收ref属性的FormInput组件,代码如下:
import React, {forwardRef, ReactElement, ReactNode, HTMLProps, useRef} from 'react' import styles from './index.module.css' import classNames from 'classnames' import { IconFa } from '../icon-fa' import { useStableId } from '../use-stable-id' export interface Props extends HTMLProps<HTMLInputElement> { // An icon component to show on the left. icon?: ReactNode // A help text tooltip to show on the right. help?: string // A clear text tooltip to show on the right. clear?: string // Pass an onHelp click handler to show a help button. onHelp?: () => void // Pass an onClear click handler to show a clear button. onClear?: () => void errorMessage?: ReactNode label?: string hideLabel?: boolean } export const FormInput = forwardRef<HTMLInputElement, Props>(function FormInput(props, ref): ReactElement { const { id, icon, help, hideLabel, errorMessage, onHelp, onClear, ...rest } = props const stableId = useStableId() const inputId = id ? id : stableId console.log(ref.current) const inputClassName = classNames( styles.input, icon && styles.hasLeftIcon, (help || onHelp || onClear) && styles.hasRightIcon ) const labelClassName = classNames( styles.label, Boolean(props.value) && styles.hasValue, props['aria-invalid'] && styles.hasError, props.className ) return ( <div className={labelClassName}> {!hideLabel && ( <label className={styles.labelText} htmlFor={inputId}> {props.label} {props.required && '*'} </label> )} <div className="relative"> {icon && <div className={styles.leftIcon}>{icon}</div>} <input {...rest} id={inputId} ref={ref} aria-invalid={props['aria-invalid']} aria-label={props.hideLabel ? props.label : undefined} className={inputClassName} /> {onClear && <ClearIcon {...props} />} {!onClear && (help || onHelp) && <HelpIcon {...props} />} </div> {props['aria-invalid'] && <span className={styles.error}>{errorMessage}</span>} </div> ) }) function HelpIcon(props: Props) { if (props.onHelp) { return ( <button type="button" className={styles.rightIcon} aria-label={props.help} onClick={props.onHelp}> <IconFa icon={['far', 'info-circle']} title={props.help} /> </button> ) } return ( <div className={styles.rightIcon} title={props.help}> <IconFa icon={['far', 'info-circle']} /> </div> ) } function ClearIcon(props: Props) { return ( <button type="button" className={styles.rightIcon} aria-label={props.clear} onClick={props.onClear}> <IconFa icon={['far', 'times']} title={props.clear} /> </button> ) }
我尝试用以下代码判断输入框是否激活:
const active = document.activeElement === ref.current
但出现TypeScript错误:
Property 'current' does not exist on type '(instance: HTMLInputElement | null) => void'.
请问该如何正确使用这个forwardedRef?
解决方案
错误原因
React的forwardRef接收的ref参数有两种合法类型:
- 对象式ref:通过
useRef创建的对象,自带current属性 - 回调式ref:一个接收DOM元素实例作为参数的函数
TypeScript报错是因为当前ref被推断为回调式ref,因此不存在current属性。
推荐实现方式:内部ref + useImperativeHandle
通过创建内部ref管理输入框元素,同时用useImperativeHandle将内部ref暴露给外部,兼容两种ref类型:
修改后的FormInput组件代码:
import React, {forwardRef, ReactElement, ReactNode, HTMLProps, useRef, useImperativeHandle, useEffect} from 'react' import styles from './index.module.css' import classNames from 'classnames' import { IconFa } from '../icon-fa' import { useStableId } from '../use-stable-id' export interface Props extends HTMLProps<HTMLInputElement> { icon?: ReactNode help?: string clear?: string onHelp?: () => void onClear?: () => void errorMessage?: ReactNode label?: string hideLabel?: boolean } export const FormInput = forwardRef<HTMLInputElement, Props>(function FormInput(props, ref): ReactElement { const { id, icon, help, hideLabel, errorMessage, onHelp, onClear, ...rest } = props const stableId = useStableId() const inputId = id ? id : stableId // 内部ref,稳定管理输入框DOM实例 const inputRef = useRef<HTMLInputElement>(null) // 判断输入框是否处于激活状态 const isActive = document.activeElement === inputRef.current // 将内部ref的实例暴露给外部forwarded ref useImperativeHandle(ref, () => inputRef.current!, []) // 兼容回调式ref的场景 useEffect(() => { if (typeof ref === 'function') { ref(inputRef.current) } }, [ref]) const inputClassName = classNames( styles.input, icon && styles.hasLeftIcon, (help || onHelp || onClear) && styles.hasRightIcon ) const labelClassName = classNames( styles.label, Boolean(props.value) && styles.hasValue, props['aria-invalid'] && styles.hasError, props.className ) return ( <div className={labelClassName}> {!hideLabel && ( <label className={styles.labelText} htmlFor={inputId}> {props.label} {props.required && '*'} </label> )} <div className="relative"> {icon && <div className={styles.leftIcon}>{icon}</div>} <input {...rest} id={inputId} ref={inputRef} // 绑定到内部ref aria-invalid={props['aria-invalid']} aria-label={props.hideLabel ? props.label : undefined} className={inputClassName} /> {onClear && <ClearIcon {...props} />} {!onClear && (help || onHelp) && <HelpIcon {...props} />} </div> {props['aria-invalid'] && <span className={styles.error}>{errorMessage}</span>} </div> ) }) // HelpIcon和ClearIcon代码保持不变 function HelpIcon(props: Props) { if (props.onHelp) { return ( <button type="button" className={styles.rightIcon} aria-label={props.help} onClick={props.onHelp}> <IconFa icon={['far', 'info-circle']} title={props.help} /> </button> ) } return ( <div className={styles.rightIcon} title={props.help}> <IconFa icon={['far', 'info-circle']} /> </div> ) } function ClearIcon(props: Props) { return ( <button type="button" className={styles.rightIcon} aria-label={props.clear} onClick={props.onClear}> <IconFa icon={['far', 'times']} title={props.clear} /> </button> ) }
关键改动说明
- 创建内部
inputRef直接绑定到<input>元素,确保能稳定获取DOM实例 - 使用
useImperativeHandle将内部ref的current暴露给外部,保证对象式ref正常工作 - 通过
useEffect处理回调式ref,当ref变化时调用回调传入DOM实例 - 判断激活状态时直接使用内部
inputRef.current,彻底避免类型冲突
替代方案:直接判断ref类型
如果不想使用useImperativeHandle,可以手动判断ref类型,但仍需内部ref存储元素:
const inputRef = useRef<HTMLInputElement>(null) // 绑定input到inputRef // 处理外部ref useEffect(() => { if (typeof ref === 'object' && ref !== null) { (ref as React.MutableRefObject<HTMLInputElement | null>).current = inputRef.current } else if (typeof ref === 'function') { ref(inputRef.current) } }, [ref]) // 判断激活状态 const isActive = document.activeElement === inputRef.current
这种方式和推荐方案逻辑一致,但useImperativeHandle是React官方推荐的ref封装方式,代码更简洁规范。
内容的提问来源于stack exchange,提问作者Ludwig
相关产品推荐
相关产品推荐

