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

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参数有两种合法类型:

  1. 对象式ref:通过useRef创建的对象,自带current属性
  2. 回调式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>
  )
}

关键改动说明

  1. 创建内部inputRef直接绑定到<input>元素,确保能稳定获取DOM实例
  2. 使用useImperativeHandle将内部ref的current暴露给外部,保证对象式ref正常工作
  3. 通过useEffect处理回调式ref,当ref变化时调用回调传入DOM实例
  4. 判断激活状态时直接使用内部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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 07:25:22