1. Wait wait...what is it?
useImperativeHandle
allows us to pass values and functions from a Child component to a Parent using a ref
.
From there, the Parent can either use it itself or pass it to another Child.
Note that you can only pass a ref as a prop to a child that wraps its component in
forwardRef
.
Code examples are much better than words when it comes to understanding so here is one:
// Parent Component
const App = () => {
const ref = useRef();
return (
<div>
<ComponentWithButton ref={ref} />
<button onClick={() => ref.current.increment()}>another button</button>
</div>
);
};
// Child Component
const ComponentWithButton = forwardRef((props, ref) => {
useImperativeHandle(ref, () => ({increment}))
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
return (
<div>
<button onClick={increment}>click</button>
<h2>Count: {count}</h2>
</div>
)
})
In the example above, we are changing the count variable in the parent component with the help of useImperativeHandle
and forwardRef
.
2. Why?
The general pattern in React is to have a unidirectional flow of data.
In cases where bidirectional dataflow is needed, we can use libraries such as Redux
or React context
.
However, in some cases, using those is simply just overkill.
This is where useImperativeHandle
comes in.
Now that we have some understanding of the hook and when we want to use it let's move to the Real Life Example...
We have a Settings
page that allows the user to update and edit his information and notification preferences.
The component has sections
and every section is a form that is responsible for changing data related to the user (A section for his profile info, his privacy settings, and his notifications settings).
const Section = ({ name, text, fields, schema }) => {
const { control, handleSubmit, reset, formState } = useForm({
mode: 'onChange',
defaultValues: fields.reduce((acc, field) => ({ ...acc, [field.name]: field.defaultValue }), {})
});
return (
<section className={styles.section}>
<Title text={text} />
<form onSubmit={handleSubmit(onSubmit)}>
{fields.map(field => (
<Field key={field.name} {...field} control={control} />
))}
</form>
</section>
);
};
Every section
is rendered in the Settings
component - The Parent Component:
const Settings = () => (
<main className={styles.main}>
{SECTIONS.map(section => (
<Section key={section.name} {...section} />
))}
</main>
);
Supposedly, everything is fine a Parent component that renders children...but what happens when we want to trigger the submit function of every section by clicking a global button?
We will need some way to allow the parent to control, that's where useImperativeHandle
comes in.
We will add the hook in the Section
component and wrap it with the forward ref so that we can pass a ref from Settings
:
const Section = React.forwardRef(({ name, text, fields, schema },ref) => {
const { control, handleSubmit, reset, formState } = useForm({
mode: 'onChange',
defaultValues: fields.reduce((acc, field) => ({ ...acc, [field.name]: field.defaultValue }), {})
});
useImperativeHandle(ref, () => ({
submit() {
handleSubmit(onSubmit)();
}
}));
return (
<section className={styles.section}>
<Title text={text} />
<form onSubmit={handleSubmit(onSubmit)}>
{fields.map(field => (
<Field key={field.name} {...field} control={control} />
))}
</form>
</section>
);
});
With the help of the hook we are able to create some sort of an API for the parent to use, in this example we are exposing the submit()
function that we we'll be able to call.
Now our Settings
component will look like so:
const Settings = () => {
const refProfile = useRef();
const refNotifications = useRef();
const refPrivacy = useRef();
// The SECTIONS object is a configuration object that will
// hold the refs among the rest of the data
const onSubmitAll = () => {
SECTIONS.forEach(({ ref }) => {
ref.current.submit();
});
};
return (
<main className={styles.main}>
{SECTIONS.map(section => (
// The ref of each section i passed here in the spread
// operation.
<Section key={section.name} {...section} />
))}
</main>
);
}
That's it! We did it!
We Passed the control back to the parent without importing or using a more complex library.
3. Conclusion
I don't mean to disappoint but React doesnβt recommend using this hook. (there will most likely be another way in which you can do this without using the hook.)
Full disclosure, I ended up changing the component structure
But! nonetheless, it was super fun learning about this mysterious hook that is rarely used.
I hope you had fun too ππΌ Thank you for reading!!
Top comments (6)
I don't quite agree with your comparison of Ref and Redux. Because my understanding is that Ref is more to operate on the HTML DOM.
Yes usually
useRef
is used for DOM nodes but in this case (with the help offorwardRef
anduseImerativeHandle
) we are able to create some sort of an API the parent can use, much like the global functions and state Redux gives us.If you want to read more I found this link: reactjs.org/docs/refs-and-the-dom....
Great read!
Even though it is advised against and I will probably not use it in my career. Reading of your journey with the task was nice and very relatable :)
Thank you Yariv!!
Can you show a typescript's
ref
correctly typed foruseImperativeHandle
?The
ref
's type will be:React.Ref<{ submit(): void }>
for more information check out this site:react-typescript-cheatsheet.netlif...