React offers an awesome toolset that allows you to effectively break up and modularize your applications into smaller pieces that work together. This allows us developers to breakout functionality and keep it in one location.
While this way of architecting allows for amazing levels of flexibility and maintainability, we all will eventually hit this scenario: What if I want one custom React component to trigger a function that lives in one of its custom Child components?
Using React's forwardRef
API along with the built-in hook useImperativeHandle
, this is simple to do!
This tutorial assumes you have some basic knowledge of React and how to use it.
Setting up our Components
To demonstrate, we're going to build a simple component that presents you with some color options and a box. Clicking on the color buttons will change the color of the box.
The header and buttons of our component will live in the Parent component, while the color-shifting box and the functionality to change the box's color will be held in the Child component. Here's what that looks like:
import { useState } from 'react'
const Parent = () => {
return (
<div className="flex flex-col gap-4 min-h-screen bg-gray-200 justify-center items-center">
<h2 className="text-gray-500 text-2xl font-bold text-30">What color should the box be?</h2>
<div className="flex justify-between w-80">
<button className="bg-blue-400 py-2 px-4 focus:outline-none rounded-xl text-white font-bold">Blue</button>
<button className="bg-green-400 py-2 px-4 focus:outline-none rounded-xl text-white font-bold">Green</button>
<button className="bg-red-400 py-2 px-4 focus:outline-none rounded-xl text-white font-bold">Red</button>
<button className="bg-yellow-400 py-2 px-4 focus:outline-none rounded-xl text-white font-bold">Yellow</button>
</div>
<Child/>
</div>
)
}
const Child = () => {
const [ color, setColor ] = useState('bg-blue-300')
const changeColor = color => {
setColor(color)
}
return <div className={`w-40 h-40 transition-colors duration-900 ease-in-out rounded-2xl ${color}`}></div>
}
NOTE: I'm using TailwindCSS for some quick styling
There's nothing too crazy going on here, just rendering out our Parent and Child. The Child has a function to update the color of its box, and some state to hold that setting. For now the buttons don't do anything, the box is blue... boring! Let's bring this thing to life!
NOTE: I'm using this as an example because it's easy to understand and visualize the result. In a real-world setting, you'd be better off passing the color through to the Child component as a prop.
Reference the Child
First thing's first, we need to somehow reference the Child component to get access to its properties. React's useRef
hook does exactly that. To create a reference to the Child component, we'll need to import that hook from react
, create a reference, and apply that reference to the component.
// Added useRef to our imports
import { useState, useRef } from 'react'
const Parent = () => {
// Set up our reference
const boxRef = useRef(null)
return (
<div className="flex flex-col gap-4 min-h-screen bg-gray-200 justify-center items-center">
<h2 className="text-gray-500 text-2xl font-bold text-30">What color should the box be?</h2>
<div className="flex justify-between w-80">
<button onClick={() => boxRef.current.changeColor('bg-blue-300')} className="bg-blue-400 py-2 px-4 focus:outline-none rounded-xl text-white font-bold">Blue</button>
<button onClick={() => boxRef.current.changeColor('bg-green-300')} className="bg-green-400 py-2 px-4 focus:outline-none rounded-xl text-white font-bold">Green</button>
<button onClick={() => boxRef.current.changeColor('bg-red-300')} className="bg-red-400 py-2 px-4 focus:outline-none rounded-xl text-white font-bold">Red</button>
<button onClick={() => boxRef.current.changeColor('bg-yellow-300')} className="bg-yellow-400 py-2 px-4 focus:outline-none rounded-xl text-white font-bold">Yellow</button>
</div>
{/* Apply the reference to our component */}
<Child ref={boxRef}/>
</div>
)
}
We now have a reference set up that should give us access to the properties of the Child. This reference has a property named .current
that is set to value of the DOM node of the component it is attached to, giving it access to its properties.
I went ahead and added the click-handlers on each button to trigger the changeColor
function in the Child component. Everything seems to be hooked up so we should be good to go, right? Let's try it out:
Oof, it blew up! π₯ What's going on?
The reason this won't work, and the thing that makes this process tricky, is that the ref
property on our <Child/>
component is not a normal "prop". React handles ref
differently than it handles most other props and doesn't pass it through to the Child in the props object.
forwardRef
To The Rescue
To get this to work properly, we need to "forward" our ref to the Child component. Luckly, React has a nice API called forwardRef
that allows exactly that.
To use this API, we need to import it from react
and wrap our Child component in the forwardRef
function. This function takes in props
and ref
parameters and returns the Child component.
// Added forwardRef to the import list
import { forwardRef, useState, useRef } from 'react'
const Child = forwardRef((props, ref) => {
const [ color, setColor ] = useState('bg-blue-300')
const changeColor = color => {
setColor(color)
}
return <div className={`w-40 h-40 transition-colors duration-900 ease-in-out rounded-2xl ${color}`}></div>
})
This will pass along our ref to the Child component, but now we need to expose our changeColor
function to the Parent component through that ref. To do that we will need to use useImperativeHandle
, a hook that React provides. This hook takes in a ref
param and a function that allows you to expose custom properties to the Parent through that ref. Here it is in action:
// Added useImperativeHandle to our imports
import { forwardRef, useState, useRef, useImperativeHandle } from 'react'
const Child = forwardRef((props, ref) => {
const [ color, setColor ] = useState('bg-blue-300')
useImperativeHandle(ref, () => ({
changeColor: color => {
setColor(color)
}
}))
return <div className={`w-40 h-40 transition-colors duration-900 ease-in-out rounded-2xl ${color}`}></div>
})
We now have forwarded our ref into the Child component and customized the the instance that is exposed to the Parent component, giving it access to a function that will update the Child's state to change the color of our box.
Save that and give 'er a go!
Fancy! Our "handle" into the Child component is accessible from our Parent component and allows us to update the child's state through the function we've exposed via that "handle".
Here's a look at both completed functions:
import { forwardRef, useState, useRef, useImperativeHandle } from 'react'
const Parent = () => {
// Set up our reference
const boxRef = useRef(null)
return (
<div className="flex flex-col gap-4 min-h-screen bg-gray-200 justify-center items-center">
<h2 className="text-gray-500 text-2xl font-bold text-30">What color should the box be?</h2>
<div className="flex justify-between w-80">
<button onClick={() => boxRef.current.changeColor('bg-blue-300')} className="bg-blue-400 py-2 px-4 focus:outline-none rounded-xl text-white font-bold">Blue</button>
<button onClick={() => boxRef.current.changeColor('bg-green-300')} className="bg-green-400 py-2 px-4 focus:outline-none rounded-xl text-white font-bold">Green</button>
<button onClick={() => boxRef.current.changeColor('bg-red-300')} className="bg-red-400 py-2 px-4 focus:outline-none rounded-xl text-white font-bold">Red</button>
<button onClick={() => boxRef.current.changeColor('bg-yellow-300')} className="bg-yellow-400 py-2 px-4 focus:outline-none rounded-xl text-white font-bold">Yellow</button>
</div>
{/* Apply the reference to our component */}
<Child ref={boxRef}/>
</div>
)
}
const Child = forwardRef((props, ref) => {
const [ color, setColor ] = useState('bg-blue-300')
useImperativeHandle(ref, () => ({
changeColor: color => {
setColor(color)
}
}))
return <div className={`w-40 h-40 transition-colors duration-900 ease-in-out rounded-2xl ${color}`}></div>
})
Conclusion
Using React's forwardRef
API and useImperativeHandle
hook, we gain the flexibility to allow for even greater component interactions, adding to the awesome flexibility of the React library. While the example in this article was a bit overkill and added a sorta unnecessary level of complexity to an otherwise simple component, these concepts can be extremely useful when building Component libraries with Alerts, Modals, etc...
Thanks so much for giving this a read, I hope it was helpful!
If you liked this, be sure to follow me on Twitter to get updates on new articles I write!
Top comments (3)
Usually, in situations like this, you'd want to consider lifting the state out of the child component and into the parent. That way, the child is a controlled component that receives a prop telling it what color it should use. The parent maintains the state for the current color, which changes whenever you click one of the control buttons. It passes this color along to the child, which then styles itself. The advantage here is that it's a top-down data flow, with a single source of truth for the state. The child component is also more reusable ("dumb") because it just accepts a color and styles itself; it doesn't worry about the logic of how that color came to be.
The approach described in this article is a bit unconventional because the parent is reaching into its child and relying on that child's implementation to do some work (in this case, it assumes that the child in fact has a method named
changeColor
). With a top-down data flow, the relationship between the child and parent is more explicit: the parent has a state, and the child takes props. In this case, though, it's more like the child exposing a method to the parent through a ref.There definitely are legitimate use cases for refs, but I don't think this is one of them.
Certainly agree 100%! Thanks for pointing this out, the demo in the article was just a simple example of βhowβ to set up and use the refs. Definitely not the ideal situation to actually use them though π
Thank you so much, @sabinthedev ! That is exactly the way to solve my task, that I've been looking for.