Refs in React: from access to DOM to imperative API

Nadia Makarevich
12 min readApr 4, 2023

--

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 the 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 in this codesandbox.

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 in this codesandbox.

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 in this codesandbox.

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.

--

--

Responses (2)