forwardRef()
You must learn useRef()
before getting into this chapter.
What Is forwardRef()
?
forwardRef()
is a built-in function that is used to forward the reference of a component to a specific target. To be more specific, it is used to change the default target of reference when ref
attribute is used on child components.
There are two generic types in forwardRef<T, P>()
; T
is the type of value being exposed to parent, and P
is the type of component props.
Example
forwardRef()
is essential for us to use ref
attribute on function-child components. However, unlike how ref
works on class components, we still can't get the instance of a function component with forwardRef()
alone. We can only get the instance of a DOM node, or passing the reference down to a deeper component at most.
For example, if we have a component like this:
import { useRef } from 'react'
interface IInputGroupProps {
label: string
}
export const InputGroup = ({ label }: IInputGroupProps) => {
return (
<div>
<label>{label}</label>
<input />
</div>
)
}
In the parent component, we may use it like this:
import { InputGroup } from './InputGroup'
export const Parent = () => {
return (
<div>
<InputGroup label="First Name" />
<InputGroup label="Last Name" />
</div>
)
}
The result would look like this:
Everything works well at first, however, we're now required to add a new feature — focuses on "Last Name" input when a button in Parent
is clicked. Since the <input>
tag is placed inside a child component, there doesn't seem to be an elegant way to do this.
This is where forwardRef()
could be useful. We could use it to make ref
attribute available on function components, and forward the reference to the <input>
inside InputGroup
. For example:
import { forwardRef } from 'react'
interface IInputGroupProps {
label: string
}
export const InputGroup = forwardRef<HTMLInputElement, IInputGroupProps>(
({ label }, ref) => {
return (
<div>
<label>{label}</label>
<input ref={ref} />
</div>
)
}
)
As you can see, ref
is not a member of props; instead, it is put in the second argument of forwardRef()
for us to use. After binding the ref
to the <input>
tag, we can finally use reference to get the instance of <input>
from parent:
import { useRef } from 'react'
import { InputGroup } from './InputGroup'
export const Parent = () => {
const lastNameInput = useRef<HTMLInputElement>(null)
const focusLastNameInput = () => {
lastNameInput.current?.focus()
}
return (
<div>
<InputGroup label="First Name" />
<InputGroup
ref={lastNameInput}
label="Last Name"
/>
<button onClick={focusLastNameInput}>
Focus Last Name Input
</button>
</div>
)
}
Does forwardRef()
work with class components?
Yes, but we don't recommend this because some weird tricks are inevitable in order to make it work. For example:
import { Component, forwardRef } from 'react'
interface IInputGroupProps {
label: string
}
interface IInputGroupState {}
export const InputGroup = forwardRef<HTMLInputElement, IInputGroupProps>(
(props, ref) => {
class MyComponent extends Component<IInputGroupProps, IInputGroupState> {
render() {
return (
<div>
<label>{this.props.label}</label>
<input ref={ref} />
</div>
)
}
}
return <MyComponent {...props} />
}
)
In order to use the ref
from forwardRef()
in a class component, we have to wrap the definition of class component inside forwardRef()
(or do something similar).
Furthermore, in this example, since MyComponent
(a component) is defined inside InputGroup
(also a component), every time InputGroup
re-renders, MyComponent
is going to be redeclared again. Thus, the "old" <MyComponent {...props} />
will unmount, and the "new" <MyComponent {...props} />
will mount within every render, causing you to lose everything in the old MyComponent
.
To solve this problem, the easiest solution would be to memoize the definition of MyComponent
before the very first render and only use it since then. For example:
import { Component, forwardRef } from 'react'
let MemoizedComponent: Component
export const InputGroup = forwardRef(
(props, ref) => {
class MyComponent extends Component {
// ...
}
if (!MemoizedComponent) {
MemoizedComponent = MyComponent
}
return <MemoizedComponent {...props} />
}
)
All in all, to to make things easier, just use the built-in ref
in a class component!
useImperativeHandle()
Even though the name makes it sound like it's something related to event handling or drag and drop, it actually has nothing to do with them. useImperativeHandle()
is a built-in hook that is used to change the value being exposed to parent when ref
attribute is used on child components; this hook must be used together with forwardRef()
(because that's the only way to get the ref
being passed down from parent).
- There are three arguments in
useImperativeHandle()
:- The
ref
being passed down from parent; that is, the second argument offorwardRef()
. - A function that is used to expose a value to parent.
- An optional dependency array
dependencies
that determines when should the exposed value be re-computed. Similar touseEffect()
, by defaultdependencies
isundefined
, meaning the exposed value is re-computed within every render.
- The
- There are two optional generic types in
useImperativeHandle<T, R extends T>()
;T
is the type of reference (theT
inuseRef<T>()
from parent), andR
is the type of value to be exposed to parent which must extendsT
.
The way useImperativeHandle()
works is like "intercepting" the ref
and returning anything we want to expose to parent.
Example
With the help of useImperativeHandle()
, we can now call the methods defined in children from parent, just like what ref
attribute could do on class components.
We cannot stress this enough; only use this when standard props/states cannot fulfill your requirements, or when using standard props/states is inconvenient. The example below is the function component version of one of the example we've mentioned in useRef()
.
import { useRef } from 'react'
import { Child, IChild } from './Child'
export const Parent = () => {
const child = useRef<IChild>(null)
const makeChilGetOld = () => {
child.current?.getOld()
}
return (
<div>
<Child ref={child} />
<button onClick={makeChilGetOld}>
Make Child Get Old
</button>
</div>
)
}
import { forwardRef, useImperativeHandle, useState } from 'react'
export interface IChild {
getOld: () => void
}
export const Child = forwardRef<IChild>((props, ref) => {
const [age, setAge] = useState(5)
const getOld = () => {
setAge((prev) => prev + 1)
}
useImperativeHandle(ref, () => ({ getOld }), [])
return (
<h1>Hello, I am {age} years old</h1>
)
})