Next.js starter your AI actually understands. Ship internal tools in days not weeks. Pre-order $199 $499 → [Get it now]

How to type forwardRef in React with TypeScript

Accessing a child component’s DOM node or instance is a frequent requirement in React development, yet doing so safely in TypeScript often leads to complex compiler errors.
With over 25 years of experience in software development and as the creator of CoreUI, I’ve implemented thousands of typed components that require precise ref handling for accessibility and animation.
The most efficient and modern way to solve this is by leveraging the React.forwardRef generic types, specifically React.forwardRef<T, P>.
This approach ensures that both your props and the forwarded ref are strictly typed, preventing runtime errors and providing a seamless developer experience in large-scale applications.

Type forwardRef by providing the element type as the first generic argument and the props type as the second: React.forwardRef<ElementType, PropsType>.

import React, { forwardRef } from 'react'

interface MyInputProps {
  label: string
}

const MyInput = forwardRef<HTMLInputElement, MyInputProps>((props, ref) => (
  <div>
    <label>{props.label}</label>
    <input ref={ref} type='text' />
  </div>
))

export default MyInput

In this snippet, forwardRef takes two type arguments: the first is the type of the element being referenced (e.g., HTMLInputElement), and the second is the type of the component’s props. Inside the component function, the ref parameter is automatically typed as React.ForwardedRef<HTMLInputElement>. This ensures that any parent component passing a ref to MyInput must provide a compatible ref type, maintaining strict type safety across your component tree.

Best Practice Note:

Always place the Ref type before the Props type in the generic arguments, as this is the order expected by React’s internal definitions. At CoreUI, we use this pattern extensively in our form components like CFormInput to ensure that developers get full IDE support when interacting with the underlying DOM elements.

1. Typing forwardRef with HTML Elements

The most common use case for forwardRef is exposing a standard HTML element, such as an input or a button, to a parent component. This is essential for focusing inputs programmatically or measuring element dimensions.

import React, { forwardRef, ReactNode } from 'react'

/**
 * Interface for the component props.
 * We include ReactNode for children to allow flexible content.
 */
interface CustomButtonProps {
  children: ReactNode
  onClick?: () => void
  className?: string
}

/**
 * We type forwardRef with <HTMLButtonElement, CustomButtonProps>.
 * The first type is the element the ref will point to.
 * The second type is the props interface.
 */
const CustomButton = forwardRef<HTMLButtonElement, CustomButtonProps>(
  (props, ref) => {
    const { children, onClick, className } = props

    return (
      <button
        ref={ref}
        onClick={onClick}
        className={className}
        style={{ padding: '10px 20px', borderRadius: '4px' }}
      >
        {children}
      </button>
    )
  }
)

// Adding a displayName is a best practice for easier debugging in React DevTools
CustomButton.displayName = 'CustomButton'

export default CustomButton

In this example, the parent component can now create a useRef<HTMLButtonElement>(null) and pass it to CustomButton. TypeScript will correctly allow access to all button-specific properties and methods on that ref.

2. Integrating with CoreUI Components

When building complex forms, you often need to wrap library components like those in CoreUI. Typing these wrappers correctly ensures you don’t lose the benefits of the library’s built-in types.

import { forwardRef, InputHTMLAttributes } from 'react'
import { CFormInput, CFormLabel } from '@coreui/react'

/**
 * Extending standard input props allows our component
 * to accept all native HTML input attributes.
 */
interface StyledInputProps extends InputHTMLAttributes<HTMLInputElement> {
  label: string
  helperText?: string
}

/**
 * forwardRef ensures the ref is passed down to the internal CFormInput.
 * This is crucial for form libraries like React Hook Form.
 */
const StyledInput = forwardRef<HTMLInputElement, StyledInputProps>(
  ({ label, helperText, ...rest }, ref) => {
    return (
      <div className='mb-3'>
        <CFormLabel htmlFor={rest.id}>{label}</CFormLabel>
        <CFormInput 
          ref={ref} 
          {...rest} 
        />
        {helperText && (
          <div className='form-text'>{helperText}</div>
        )}
      </div>
    )
  }
)

StyledInput.displayName = 'StyledInput'

export default StyledInput

Using React.InputHTMLAttributes<HTMLInputElement> allows your component to be “spread-compatible” with native inputs, while the forwardRef generic ensures the ref is correctly typed for the CFormInput component.

3. Forwarding Refs to Custom Components

Sometimes the “leaf” node isn’t a DOM element but another custom component that also uses forwardRef. In these cases, you need to match the ref type expected by the child.

import React, { forwardRef, useRef, useImperativeHandle } from 'react'

/**
 * The inner component defines what it exposes via its ref.
 */
interface InnerComponentProps {
  name: string
}

const InnerComponent = forwardRef<HTMLDivElement, InnerComponentProps>(
  (props, ref) => <div ref={ref}>Hello, {props.name}</div>
)

/**
 * The wrapper component forwards its ref directly to InnerComponent.
 * The ref type must remain HTMLDivElement.
 */
const WrapperComponent = forwardRef<HTMLDivElement, InnerComponentProps>(
  (props, ref) => {
    // We can perform logic here before forwarding
    console.log('Rendering wrapper for:', props.name)

    return <InnerComponent ref={ref} {...props} />
  }
)

WrapperComponent.displayName = 'WrapperComponent'

export default WrapperComponent

This pattern is useful for creating higher-order components or layout wrappers that shouldn’t break the ref chain. It ensures that no matter how many layers deep your component is, the parent can still reach the base DOM element.

4. Using useImperativeHandle with TypeScript

In some scenarios, you don’t want to expose the raw DOM node, but rather a custom API. useImperativeHandle allows you to define exactly what the ref.current object contains.

import React, { forwardRef, useImperativeHandle, useRef } from 'react'

/**
 * Define the interface for the API we want to expose.
 */
export interface MyComponentHandle {
  focus: () => void
  reset: () => void
}

interface MyComponentProps {
  initialValue?: string
}

/**
 * The first generic argument is now our custom interface, not an HTML element.
 */
const CustomRefComponent = forwardRef<MyComponentHandle, MyComponentProps>(
  (props, ref) => {
    const inputRef = useRef<HTMLInputElement>(null)

    // useImperativeHandle maps the forwarded ref to our custom object
    useImperativeHandle(ref, () => ({
      focus: () => {
        inputRef.current?.focus()
      },
      reset: () => {
        if (inputRef.current) {
          inputRef.current.value = ''
        }
      }
    }))

    return (
      <input 
        ref={inputRef} 
        defaultValue={props.initialValue} 
        type='text' 
      />
    )
  }
)

CustomRefComponent.displayName = 'CustomRefComponent'

export default CustomRefComponent

When a parent uses this component, ref.current will strictly follow the MyComponentHandle interface. This is a powerful way to encapsulate component logic while still providing a controlled imperative interface.

5. Handling Optional Refs and Defaults

In TypeScript, the ref passed to forwardRef can technically be null. It is important to handle this if you are performing manual calculations inside your component.

import { forwardRef, useCallback, useEffect, useRef, ForwardedRef } from 'react'

interface ObserverProps {
  onVisible: () => void
}

/**
 * Helper to merge multiple refs into a single callback ref.
 */
function mergeRefs<T>(...refs: Array<ForwardedRef<T> | null>) {
  return (node: T | null) => {
    for (const ref of refs) {
      if (typeof ref === 'function') {
        ref(node)
      } else if (ref) {
        (ref as React.MutableRefObject<T | null>).current = node
      }
    }
  }
}

/**
 * Using forwardRef with a standard div but adding internal logic.
 */
const ObservedDiv = forwardRef<HTMLDivElement, ObserverProps>(
  (props, ref) => {
    const internalRef = useRef<HTMLDivElement>(null)

    useEffect(() => {
      const target = internalRef.current
      if (!target) return

      const observer = new IntersectionObserver(([entry]) => {
        if (entry.isIntersecting) {
          props.onVisible()
        }
      })

      observer.observe(target)
      return () => observer.disconnect()
    }, [props.onVisible])

    const mergedRef = useCallback(mergeRefs<HTMLDivElement>(ref, internalRef), [ref])

    return (
      <div 
        ref={mergedRef} 
        style={{ height: '100px', background: '#f4f4f4' }}
      >
        Scroll to me
      </div>
    )
  }
)

ObservedDiv.displayName = 'ObservedDiv'

export default ObservedDiv

Handling the dual nature of ref (callback or object) is a common pattern in advanced UI libraries. While TypeScript makes this verbose, it ensures that your component remains robust regardless of how the parent chooses to consume the ref.

6. forwardRef with Generic Props

One limitation of React.forwardRef is that it doesn’t support generic props natively (e.g., Component<T>). To achieve this, you often need to use a type cast or a helper function.

import { forwardRef, ForwardedRef, ReactElement, ReactNode } from 'react'

interface SelectProps<T> {
  items: T[]
  renderItem: (item: T) => ReactNode
  onSelect: (item: T) => void
}

/**
 * We define the component logic first.
 */
function SelectInner<T>(
  props: SelectProps<T>,
  ref: ForwardedRef<HTMLUListElement>
) {
  return (
    <ul ref={ref} className='list-group'>
      {props.items.map((item, index) => (
        <li 
          key={index} 
          onClick={() => props.onSelect(item)}
          className='list-group-item'
        >
          {props.renderItem(item)}
        </li>
      ))}
    </ul>
  )
}

/**
 * Then we cast it to support generics while keeping forwardRef functionality.
 */
export const GenericSelect = forwardRef(SelectInner) as <T>(
  props: SelectProps<T> & { ref?: ForwardedRef<HTMLUListElement> }
) => ReactElement

// Example usage:
// <GenericSelect<string> items={['A', 'B']} ... />

This “hack” is widely accepted in the TypeScript community to overcome the lack of generic support in forwardRef. It allows you to maintain the strict typing of your data while still exposing the underlying list element ref.

For more information on React patterns, you might find our guide on how to use typescript with react hooks useful. If you are building admin interfaces, our Admin Dashboard Template provides many production-ready examples of these patterns.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author