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:
export default function App() {
// We'd like to call scrollIntoView on MyInput's rendered element
return <MyInput />
}
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.
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:
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.
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.