Originally published at https://www.developerway.com. The website has more articles like this đ
One of the many beautiful things about React is that it abstracts away the complexity of dealing with real DOM. Now, instead of manually querying elements, scratching our heads over how to add classes to those elements, or struggling with browser inconsistencies, we can just write components and focus on user experience. There are, however, still cases (very few though!) when we need to get access to the actual DOM.
And when it comes to actual DOM, the most important thing to understand and learn how to use properly is Ref and everything surrounding Ref. So today, letâs take a look at why we would want to get access to DOM in the first place, how Ref can help with that, what are useRef
, forwardRef
and useImperativeHandle
, and how to use them properly. Also, letâs investigate how to avoid using forwardRef
and useImperativeHandle
while still having what they give us. If you ever tried to figure out how those work, youâll understand why weâd want that đ
And as a bonus, weâll learn how to implement imperative APIs in React!
This article is also available as a YouTube video: it has fewer details but nice animations instead. Sometimes it's easier to understand a concept from a 3-second animation than from two paragraphs of text.
DOM access in React with useRef
Letâs say I want to implement a sign-up form for a conference Iâm organizing. I want people to give me their name, email and Twitter handle before I can send them the details. âNameâ and âemailâ fields I want to make mandatory. But I donât want to show some annoying red borders around those inputs when people try to submit them empty, I want the form to be cool. So instead, I want to focus the empty field and shake it a little to attract attention, just for the fun of it.
Now, React gives us a lot, but it doesnât give us everything. Things like âfocus an element manuallyâ is not part of the package. For that, we need to dust off our rusty native javascript API skills. And for that, we need access to the actual DOM element.
In the no-React world, weâd do something like this:
const element = document.getElementById("bla");
After then we can focus it:
element.focus();
Or scroll to it:
element.scrollIntoView();
Or anything else our heart desires. Some typical use cases for using native DOM API in React world would include:
- manually focusing an element after itâs rendered, like an input field in a form
- detecting a click outside of a component when showing popup-like elements
- manually scrolling to an element after it appears on the screen
- calculating sizes and boundaries of components on the screen to position something like a tooltip correctly
And although, technically, nothing is stopping us from doing getElementById
even today, React gives us a slightly more powerful way to access that element that doesnât require us to spread ids everywhere or be aware of the underlying DOM structure: refs.
Ref is just a mutable object, the reference to which React preserves between re-renders. It doesnât trigger re-render, so itâs not a replacement to state in any way, donât try to use it for it. More details on the difference between those two are in the docs.
Itâs created with useRef
hook:
const Component = () => {
// create a ref with default value null
const ref = useRef(null);
return ...
}
And the value stored in Ref will be available in âcurrentâ (and only) property of it. And we can actually store anything in it! We can, for example, store an object with some values coming from state:
const Component = () => {
const ref = useRef(null);
useEffect(() => {
// re-write ref's default value with new object
ref.current = {
someFunc: () => {...},
someValue: stateValue,
}
}, [stateValue])
return ...
}
Or, more importantly for our use case, we can assign this Ref to any DOM element and some of the React components:
const Component = () => {
const ref = useRef(null);
// assing ref to an input element
return <input ref={ref} />
}
Now, if I log ref.current
in useEffect
(itâs only available after a component is rendered), Iâll see exactly the same element that I would get if I try to do getElementById
on that input:
const Component = () => {
const ref = useRef(null);
useEffect(() => {
// this will be a reference to input DOM element!
// exactly the same as if I did getElementById for it
console.log(ref.current);
});
return <input ref={ref} />
}
And now, if I was implementing my sign-up form as one giant component, I could do something like this:
const Form = () => {
const [name, setName] = useState('');
const inputRef = useRef(null);
const onSubmitClick = () => {
if (!name) {
// focus the input field if someone tries to submit empty name
ref.current.focus();
} else {
// submit the data here!
}
}
return <>
...
<input onChange={(e) => setName(e.target.value)} ref={ref} />
<button onClick={onSubmitClick}>Submit the form!</button>
</>
}
Store the values from inputs in state, create refs for all inputs, and when âsubmitâ button is clicked, I would check whether the values are not empty, and if they are - focus the needed input.
Check out this form implementation in this codesandbox:
Passing ref from parent to child as a prop
Only in real life, I wouldnât do one giant component with everything of course. More likely than not, I would want to extract that input into its own component: so that it can be reused across multiple forms, and can encapsulate and control its own styles, maybe even have some additional features like having a label on the top or an icon on the right.
const InputField = ({ onChange, label }) => {
return <>
{label}<br />
<input type="text" onChange={(e) => onChange(e.target.value)} />
</>
}
But error handling and submitting functionality still going to be in the Form
, not input!
const Form = () => {
const [name, setName] = useState('');
const onSubmitClick = () => {
if (!name) {
// deal with empty name
} else {
// submit the data here!
}
}
return <>
...
<InputField label="name" onChange={setName} />
<button onClick={onSubmitClick}>Submit the form!</button>
</>
}
How can I tell input to âfocus itselfâ from the Form component? The ânormalâ way to control data and behaviour in React is to pass props to components and listen to callbacks. I could try to pass the prop âfocusItselfâ to InputField
that I would switch from false
to true
, but that would work only once.
// don't do this! just to demonstate how it could work in theory
const InputField = ({ onChange, focusItself }) => {
const inputRef = useRef(null);
useEffect(() => {
if (focusItself) {
// focus input if the focusItself prop changes
// will work only once, when false changes to true
ref.current.focus();
}
}, [focusItself])
// the rest is the same here
}
I could try to add some âonBlurâ callback and reset that focusItself
prop to false
when input loses focus, or play around with random values instead of boolean, or come up with some other creative solution.
Likely, there is another way. Instead of fiddling around with props, we can just create Ref in one component (Form
), pass it down to another component (InputField
), and attach it to the underlying DOM element there. Ref is just a mutable object, after all.
Form
would then create the Ref as normal:
const Form = () => {
// create the Ref in Form component
const inputRef = useRef(null);
...
}
And InputField
component will have a prop that accepts the ref, and will have input
field that accepts the ref, as usual. Only Ref, instead of being created in InputField
, will be coming from props there:
const InputField = ({ inputRef }) => {
// the rest of the code is the same
// pass ref from prop to the internal input component
return <input ref={inputRef} ... />
}
Ref is a mutable object, was designed that way. When we pass it to an element, React underneath just mutates it. And the object that is going to be mutated is declared in the Form
component. So as soon as InputField
is rendered, Ref object will mutate, and our Form
will have access to the input
DOM element in inputRef.current
:
const Form = () => {
// create the Ref in Form component
const inputRef = useRef(null);
useEffect(() => {
// the "input" element, that is rendered inside InputField, will be here
console.log(inputRef.current);
}, []);
return (
<>
{/* Pass ref as prop to the input field component */}
<InputField inputRef={inputRef} />
</>
)
}
or in our submit callback we can call the inputRef.current.focus(), exactly the same code as before.
Check out the example here:
Passing ref from parent to child with forwardRef
In case youâre wondering why I named the prop inputRef
, rather than just ref
: itâs actually not that simple. ref
is not an actual prop, it's kinda a âreservedâ name. In the old days, when we were still writing class components, if we passed ref to a class component, this componentâs instance would be the .current
value of that Ref.
But functional components donât have instances. So instead, we just get a warning in console âFunction components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?â
const Form = () => {
const inputRef = useRef(null);
// if we just do this, we'll get a warning in console
return <InputField ref={inputRef} />
}
In order for this to work, we need to signal to React that this ref
is actually intentional, we want to do stuff with it. We can do it with the help of forwardRef
function: it accepts our component and injects the ref from the ref
attribute as a second argument of the componentâs function. Right after the props.
// normally, we'd have only props there
// but we wrapped the component's function with forwardRef
// which injects the second argument - ref
// if it's passed to this component by its consumer
const InputField = forwardRef((props, ref) => {
// the rest of the code is the same
return <input ref={ref} />
})
We could even split the above code into two variables for better readability:
const InputFieldWithRef = (props, ref) => {
// the rest is the same
}
// this one will be used by the form
export const InputField = forwardRef(InputFieldWithRef);
And now the Form
can just pass ref to the InputField
component as it was a regular DOM element:
return <InputField ref={inputRef} />
Whether you should use forwardRef
or just pass ref as a prop is just a matter of personal taste: the end result is the same.
See the live example here:
Imperative API with useImperativeHandle
Okay, focusing the input from the Form
component is sorted, kinda. But we are in no way done with our cool form. Remember, we wanted to shake the input in addition to focusing when the error happens? There is no such thing as element.shake()
in native javascript API, so access to the DOM element wonât help here đ˘
We could very easily implement it as a CSS animation though:
const InputField = () => {
// store whether we should shake or not in state
const [shouldShake, setShouldShake] = useState(false);
// just add the classname when it's time to shake it - css will handle it
const className = shouldShake ? "shake-animation" : '';
// when animation is done - transition state back to false, so we can start again if needed
return <input className={className} onAnimationEnd={() => setShouldShake(false)} />
}
But how to trigger it? Again, the same story as before with focus - I could come up with some creative solution using props, but it would look weird and significantly over-complicate the Form. Especially considering that we're handling focus through ref, so we'd have two solutions for exactly the same problem. If only I could do something like InputField.shake()
and InputField.focus()
here!
And speaking of focus - why my Form
component still has to deal with native DOM API to trigger it? Isnât it the responsibility and the whole point of the InputField
, to abstract away complexities like this? Why does the form even have access to the underlying DOM element - itâs basically leaking internal implementation details. The Form
component shouldnât care which DOM element weâre using or whether we even use DOM elements or something else at all. Separation of concerns, you know.
Looks like itâs time to implement a proper imperative API for our InputField
component. Now, React is declarative and expects us all to write our code accordingly. But sometimes we just need a way to trigger something imperatively. Likely, React gives us an escape hatch for it: useImperativeHandle hook.
This hook is slightly mind-boggling to understand, I had to read the docs twice, try it out a few times and go through its implementation in the actual React code to really get what itâs doing. But essentially, we just need two things: decide how our imperative API would look like and a Ref to attach it to. For our input, itâs simple: we just need .focus()
and .shake()
functions as an API, and we already know all about refs.
// this is how our API could look like
const InputFieldAPI = {
focus: () => {
// do the focus here magic
},
shake: () => {
// trigger shake here
}
}
This useImperativeHandle
hook just attaches this object to Ref object's "current" property, that's all. This is how it does it:
const InputField = () => {
useImperativeHandle(someRef, () => ({
focus: () => {},
shake: () => {},
}), [])
}
The first argument - is our Ref, which is either created in the component itself, passed from props or through forwardRef. The second argument is a function that returns an object - this is the object that will be available as inputRef.current
. And the third argument is the array of dependencies, same as any other React hook.
For our component, letâs pass the ref explicitly as apiRef
prop. And the only thing that is left to do is to implement the actual API. For that weâd need another ref - this time internal to InputField
, so that we can attach it to the input
DOM element and trigger focus as usual:
// pass the Ref that we'll use as our imperative API as a prop
const InputField = ({ apiRef }) => {
// create another ref - internal to Input component
const inputRef = useRef(null);
// "merge" our API into the apiRef
// the returned object will be available for use as apiRef.current
useImperativeHandle(apiRef, () => ({
focus: () => {
// just trigger focus on internal ref that is attached to the DOM object
inputRef.current.focus()
},
shake: () => {},
}), [])
return <input ref={inputRef} />
}
And for âshakeâ weâll just trigger the state update:
// pass the Ref that we'll use as our imperative API as a prop
const InputField = ({ apiRef }) => {
// remember our state for shaking?
const [shouldShake, setShouldShake] = useState(false);
useImperativeHandle(apiRef, () => ({
focus: () => {},
shake: () => {
// trigger state update here
setShouldShake(true);
},
}), [])
return ...
}
And boom! Our Form
can just create a ref
, pass it to InputField
and will be able to do simple inputRef.current.focus()
and inputRef.current.shake()
, without worrying about their internal implementation!
const Form = () => {
const inputRef = useRef(null);
const [name, setName] = useState('');
const onSubmitClick = () => {
if (!name) {
// focus the input if the name is empty
inputRef.current.focus();
// and shake it off!
inputRef.current.shake();
} else {
// submit the data here!
}
}
return <>
...
<InputField label="name" onChange={setName} apiRef={inputRef} />
<button onClick={onSubmitClick}>Submit the form!</button>
</>
}
Play around with the full working form example here:
Imperative API without useImperativeHandle
If useImperativeHandle
hook still makes your eye twitch - donât worry, mine twitches too! But we donât actually have to use it to implement that functionality that we just implemented. We already know how refs work, and the fact that they are mutable. So all that we need is just to assign our API object to the ref.current
of the needed Ref, something like this:
const InputField = ({ apiRef }) => {
useEffect(() => {
apiRef.current = {
focus: () => {},
shake: () => {},
}
}, [apiRef])
}
This is almost exactly what useImperativeHandle
does under the hood anyway. And it will work exactly like before.
Actually, useLayoutEffect
might be even better here, but this is the topic for another article. For now, let's go with traditional useEffect
.
See the final example here:
Yey, a cool form with a nice shaking effect is ready, React refs are not a mystery anymore, and imperative API in React is actually a thing. How cool is that?
Just remember: Refs are an âescape hatchâ, itâs not a replacement to state or normal React data flow with props and callbacks. Use them only when there is no ânormalâ alternative. The same with the imperative way to trigger something - more likely than not normal props/callbacks flow is what you want.
Watch the article in YouTube format to solidify the knowledge đ
Originally published at https://www.developerway.com. The website has more articles like this đ
Subscribe to the newsletter, connect on LinkedIn or follow on Twitter to get notified as soon as the next article comes out.
Top comments (0)