React Ref Forwarding and TypeScript

Every now and then, you'll encounter blockers about certain React features that were previously straightforward. Mainly because you need to work on typings.

Objective

Suppose we want to run, on app mount, the scrollIntoView of the input element rendered by a child component (MyInput.tsx).

For this, we tend to use refs. Below is a contrived example:

App.tsx
export default function App() {
  // We'd like to call scrollIntoView on MyInput's rendered element
  return <MyInput />
}
MyInput.tsx
type MyInputProps = {
  value?: string
}
 
export default function MyInput({ value = undefined }: MyInputProps) {
  // Have a way for the parent to access the input via ref
  return <input value={value} />
}

Implementing ref forwarding

Let's bring our attention to the MyInput component and implementing ref forwarding to it with the proper types.

Here is an example with the component updated to an arrow function.

MyInput.tsx
import { forwardRef } from 'react'
 
type MyInputProps = {
  value?: string
}
 
const MyInput = forwardRef<HTMLInputElement, MyInputProps>(
  ({ value = undefined }, ref) => {
    return <input value={value} ref={ref} />
  }
)
 
export default MyInput

Depending on your linter configuration, you may need to keep the

function declaration

. For that we can do the following:

MyInput.tsx
import { forwardRef } from 'react'
import type { ForwardedRef } from 'react'
 
type MyInputProps = {
  value?: string
}
 
function MyInput(
  { value = undefined }: MyInputProps,
  ref: ForwardedRef<HTMLInputElement>
) {
  return <input value={value} ref={ref} />
}
 
export default forwardRef(MyInput)

Take note that depending on your component, you will need to adjust the forwarded ref. In MyInput's case, it returns an HTML input so we use the HTMLInputElement.

Accessing the ref from the parent component

Now that MyInput is ready to accept refs, we can now implement our objective - calling scrollIntoView when App mounts.

App.tsx
import { useEffect, useRef } from 'react'
import MyInput from './MyInput'
 
export default function App() {
  const inputRef = useRef<HTMLInputElement>(null)
 
  useEffect(() => {
    inputRef.current.scrollIntoView()
  }, [])
 
  return <MyInput ref={inputRef} />
}

Refactoring the ref object

Notice that we have to pass the HTMLInputElement type for each file. We may need to refactor it by exporting the type from the child component. This will also help keep the code consistent and more maintainable.

import { forwardRef } from 'react'
import type { ForwardedRef } from 'react'
 
type MyInputProps = {
  value?: string
}
 
export type MyInputRef = HTMLInputElement
 
function MyInput(
  { value = undefined }: MyInputProps,
  ref: ForwardedRef<MyInputRef>
) {
  return <input value={value} ref={ref} />
}
 
export default forwardRef(MyInput)
import { useEffect, useRef } from 'react'
import MyInput from './MyInput'
import type { MyInputRef } from './MyInput'
 
export default function App() {
  const inputRef = useRef<MyInputRef>(null)
 
  useEffect(() => {
    inputRef.current.scrollIntoView()
  }, [])
 
  return <MyInput ref={inputRef} />
}

Demo

Aaaaaand we're done! 🎉

Remember to keep refs usage at the minimum. Thanks for reading.