React: Using Refs with the useRef Hook

Implementing Refs in React with hooks, with example use cases

Refs: Component mutations in React without state

Refs in React give us a means of storing mutable values throughout a component’s lifecycle, and are often used for interacting with the DOM without the need of re-rendering a component. In other words, we do not need to rely on state management to update an element with Refs. This is very useful in a few select use cases, but is also seen as an anti pattern when used in place of state management or lifecycle method integration.

With React hooks now integrated into the framework however, class components along with their lifecycle methods can now be replaced with React hooks, where it makes sense.

The useRef hook has been implemented as a solution to use React refs within functional components. We’ll be exploring this hook in conjunction with others that work well together in this article. More specifically, we will:

  • Cover how to use the useRef hook in conjunction with functional components, and cover some use cases of the hook that are commonly used with useEffect and useLayoutEffect
  • How useRef can be used properly with concurrent React
  • Explore use cases of useRef with demo components

We’re focussed on functional components here, although this by no means suggests that they should be used over class components, that also come with their benefits.

Is it worth moving a project to functional components only?

Where functional components offer speed and boilerplate optimisations, class components offer tried-and-tested component lifecycle methods and well-known structures we have all become accustomed to (using this with class properties, etc).

To promote code conformity, developers commonly either opt for a class -component-only approach or a functional-component-only approach to a project. I personally opt for creating functional components as my default choice, and fallback to class components where they make sense, such as when I would like to explicitly define lifecycle methods, many class properties, etc.

In my experience, Typescript also seems to complement class components a lot more than functional components, being able to plug types into the class itself, as well as properties and its methods; classes have a more verbose structure to play with.

In any case, React Refs can be utilised in both classes and functional components. Let’s explore how we can utilise refs with the useRef hook.

I have also published an introduction to React Refs centred around it’s base class implementation here — it is worth a read if the reader is new to the concept of refs.

Implementing the useRef Hook

The implementation of functional component Ref has been achieved via a hook, named useRef. Let’s see how this integrated, and then explore its characteristics and when to use it.

A Ref can be defined within a component:

import React, { useRef } from 'react';...
const refContainer = useRef(initialValue);

This hook has a very simple API — arguably more simple than its class counterpart. The refContainer name (from the official documentation) has been chosen to reflect that this variable actually acts as a container for the underlying reference.

To coincide with the base class implementation of Refs, the referenced object itself is stored in a current property of this container variable. Two key facts about this current property:

  • The property is mutable
  • It can change at any time during a component lifecycle

Functional components still have the lifecycle of a class component, albeit with no lifecycle methods. The consideration of a component’s lifecycle will become important further down.

Also, the initialValue argument we’ve passed in above can be used to initialise current with a default value. This value often acts as a placeholder until we actually reference an element from the DOM, or assign it an arbitrary value.

Event though Refs are commonly used to refer to DOM elements, they can also store primitive types and objects. We will cover examples of both cases.

It is perfectly legal to pass in an initial value of null, too:

// initialising an empty referenceconst myRef = useRef(null);

Whatever current may be, we are able to log out the property at any time of the component lifecycle to see its value:

// see for yourself what the Ref is actually referencingconsole.log(refContainer.current);

Referencing a DOM element is done via the ref attribute. We do this on the JSX level within a return statement, via a ref attribute. The following does exactly that with a button element:

// referencing a `button` element...
render() {
return(
<button ref={refContainer}>
Press Me
</button>
);
}

Remember, we are referencing DOM HTML elements, not React components.

If we were referencing a button, then refContainer.current would be pointing to that <button /> DOM element, giving us access to controls like focussing / blurring the button, along with its styles and event handlers (onClick, for example, to trigger a click event).

Blurring is a term used to make an active element inactive. If a text box was active (cursor flashing inside ready for text input), we can blur that element to de-select it, either by clicking of tapping outside of that element, or programatically using features like Refs.

Managing button state with refs is good practice

Let’s touch on our first valid use case of useRef — buttons.

Buttons are a great use case to use alongside useRef, whereby manipulating the button’s state (not to be confused with component state) would not require a full component re-render.

Let’s consider a real-world scenario. Perhaps a form has been completed, and a submit button needs to be enabled from a default disabled state. Re-rendering my whole form just to do this would require me to:

  • Save all my current form values in state
  • Re-render the entire form again with those current values
  • Persist any other state that may be in child components, such as validation messages and visual indicators
  • Reset any transitions or animations that may be taking place

This is a lot of work for React to process under the hood. Simply referencing the button in the DOM to switch its disabled property makes a lot more sense here:

refContainer.current.setAttribute("disabled", true);// or refContainer.current.removeAttribute("disabled");

With this, we’ve now covered the basic API of useRef, as well as a viable use case — now let’s see how to properly implement useRef in conjunction with the useEffect hook.

Properly Implementing useRef in the Commit component phase

To fully understand how to implement useRef, we need to understand the two phases of React component execution, and how this ties into working with React refs.

A Ref can be defined in the main block of a functional component, but any side effects associated with the Ref, such as event listeners or timers, must be defined in a component’s commit phase, after the calculations that determine what will be updated in the DOM take place. Let’s visit this in more detail.

Render vs Commit phase

A component goes through two high level phases:

  • The render phase determines the changes made to the DOM from the previous render, and calls methods such as componentWillMount, render, and setState (amongst others)
  • The commit phase, as the name suggests, commits the changes (that the render phase determined) to the DOM, and calls methods including componentDidMount, componentDidUpdate, and componentDidCatch

The first phase, render, has a key characteristic that is of concern to us if we want to implement Refs: It could be called multiple times before the commit phase is executed — this is problematic, introducing unpredictability and the possibility of bugs into our apps.

Refs should be implemented in the commit phase

The commit phase on the other hand is only ever called once, and is the phase we should be defining side effects, and generally speaking things we only want to be instantiated once.

A side effect is anything that affects something outside the scope of the function being executed. These could be anything from API requests, web-sockets, timers, loggers, and even Refs.

In the event a component re-renders a number of times and re-initialises a Ref at that stage, that Ref logic will be executed the same number of times. This is more worrying when we consider Concurrent Mode in React, whereby the render phase of a component could be executed multiple times on the initial render.

This is because Concurrent mode breaks the rendering process into pieces, often pausing and resuming the work when other, more high priority, asynchronous processes need to be executed. The result of this is the possibility of render-phase lifecycle methods being called more than once (or not at all if there is an error) before committing.

This is unreliable to be defining side effects, or anything outside of the component’s scope. There are a number of things we can do to ensure we do not fall into these traps when developing.

1. Utilising Strict Mode

The first thing we can do is implement Strict Mode. Strict Mode is designed to highlight, via the console, various issues with how an app is coded, and has a full section dedicated to detecting unexpected side effects.

Strict Mode can be enabled via JXS, either throughout the entire app, or just on a certain number of child components. Wrap your entire app to apply Strict Mode throughout:

// wrapping your app in Strict ModeReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>
, document.getElementById('root'));

Like React.Fragment, Strict mode does not product any additional markup in your app.

This is my favoured approach to ensuring functional components’ logic are designed correctly, but there are other options available to us.

2. Testing your app in Concurrent Mode

A more direct approach is to develop your app in Concurrent mode, that will also flag issues as your are developing.

Referring to React’s Roadmap Blog Post published in November 2018, there are two ways to enable Concurrent mode, either wrapping a subset of elements, or the entire application with a Concurrent mode declaration:

// section of an app (not final API)<React.unstable_ConcurrentMode>
<MyComponent />
</React.unstable_ConcurrentMode>
// entire app (not final API)
ReactDOM.unstable_createRoot(domNode).render(<App />);

These are not the final APIs, but give developers at least some means of testing an asynchronous version of React apps. The blog post actually recommends using Strict Mode as a primary means of preparing your app for concurrency.

Due to the unstable nature of a work-in-progress, it is not recommended to deploy production builds of Concurrent Mode enabled apps until the final release.

With the above in mind, let’s now check out an actual completed implementation of useRef in the commit phase.

Implementing useRef, with useEffect

The solution to avoid the unpredictable Ref behaviour we’ve been discussing is to implement Ref side effects inside the useEffect or useLayoutEffect hook.

Why is this? Because as per the official documentation, side effects such as mutations, subscriptions, timers and logging, are not allowed inside the main body of a function component. All logic inside this main body are executed in the render phase, hence causing confusing bugs and inconsistencies in the UI.

useEffect on the other hand will run once after the actual DOM has been updated in the browser. useEffect therefore will run in the commit phase of the component.

A simple counter component can be created to demonstrate this, where we count each time a component is re-rendered. To force a re-render of the component upon a button click, the useReducer hook has also been implemented:

In the above example, refCount.current starts with a value of 0, and is incremented in the commit phase of a component update.

Remember, if we’d make this incrementation in the main function block, the update would have taken place in the render phase before the render function was returned, subjecting the increment to unpredictable duplication.

Now let’s look at another component that refers to a DOM element, and attaches event listeners to it via its ref. The event listeners are again defined in the useEffect hook. Furthermore, the return function of useEffect acts as a means of tidy-up, triggered as a component is unmounted, where we can remove the event listeners from the ref:

Now if you click into and out of the text input, the corresponding console.log output will notify you that the text input is being focussed and blurred.

Let’s take this concept a few steps further. The next example manipulates the classes of the button element through the refInput Ref. We have also introduced a <Wrapper /> Styled Component to define an active class, that changes the text input border and text colour:

Now we have some CSS manipulation in place without relying on state updates.

The final stage of our demo is to implement what we talked about earlier — implementing a submit button and disabling it if the value of our text input is empty. For this to happen I have introduced an additional useRef hook for the submit button itself. To toggle the disabled attribute, the setAttribute and removeAttribute Javascript APIs have been used with the refSubmit Ref.

The full solution is as follows:

The submit button’s disabled property is updated once we click (or tap) outside of the text input, or when it is blurred, a somewhat natural time to determine if the form is valid.

Additional Notes

This last section of the article will cover note-worthy tips of using useRef.

Use useLayoutEffect when useEffect causes problems

At the start of this article we mentioned the useLayoutEffect hook, that is also commonly used with useRef. useLayoutEffect fires in the same phase as the componentDidMount and componentDidUpdate class component lifecycle methods, so you may be inclined to use it instead of useEffect.

However, the official documentation recommends that developers should attempt using useEffect predominantly, and fall back to useLayoutEffect if issues occur with it. There will be some speed sacrifices with useLayoutEffect, as it is called synchronously only after all DOM mutations / updates have taken place — apart from this detail, it is identical to useEffect.

Forwarding useRef’s

Just like Forwarding Refs that are initialised in class components, it is also possible to forward Refs initialised via the useRef hook, as long as the same conventions are followed. Do not pass the ref as a “ref” prop — this is reserved attribute name in React and will cause errors. Instead, a prop named forwardRef.

...// defining `refInput` within `App`, forwarding it to `MyInput`function App () {
const refInput = useRef();
return <MyInput
forwardRef={refInput};
}

// referencing `input` element with `forwardRef` in child component
function MyInput (props) { // verifying `input` is referenced correctly after DOM updates
useLayoutEffect(() => {
console.log(props.forwardRef.current);
});
const { forwardRef } = props; return (
<input
ref={forwardRef}
type="submit"
value="Submit"
/>);
}

Toggling Focus with useRef

As well as listening to events, we can also trigger them. The demo would not be complete if we did not showcase this. In the below component, clicking a button will focus another text input, again with useRef:

// focussing an element with a button pressfunction TextInput () {const refInput = useRef();  function handleFocus () {
refInput.current.focus();
}
return (
<>
<input ref={refInput} placeholder="Input Here..." />
<button onClick={handleFocus}>Focus Input</button>
</>
);
}

Programatically focussing elements can polish a user experience, such as when a form first loads and the first input is automatically focused.

In Summary

This article has been a run down of useRef, and how to properly implement refs taking into consideration a component lifecycle. Using refs can be a handy way for the use cases we’ve discussed here:

  • To micro manage input with focus, blur, disable, and other attributes associated with form management
  • To add or remove classes from an element, perhaps controlling a transition or keyframe animation
  • The other use case of refs recommended in the official documentation is interacting with other HTML5 libraries, such as media players. Such libraries would not be accessible via React state, and Refs give us a fallback to interact directly with other elements while coinciding with a lifecycle of a component

For a more generalised introduction to React refs, please refer to my Introductory article:

Programmer and Author. Director @ JKRBInvestments.com. Creator of LearnChineseGrammar.com for iOS.

Get the Medium app