How to Use React Refs

How to utilise refs in React, with ref forwarding, callback refs and HOCs

React Refs are a useful feature that act as a means to reference a DOM element or a class component from within a parent component. This then give us a means to read and modify that element.

Perhaps the best way to describe a ref is as a bridge; a bridge that allows a component to access or modify an element a ref is attached to. Using refs give us a way to access elements while bypassing state updates and re-renders; this can be useful in some use cases, but should not be used as an alternative to props and state (as the official React docs point out). Nevertheless, refs work well in certain scenarios that we will visit in this article.

Refs also provide some flexibility for referencing elements within a child component from a parent component, in the form of ref forwarding — we will also explore how to do this here, including how to inject refs from HOCs into their wrapped components.

Refs at a high level

Refs are usually defined in the constructor of class components, or as variables at the top level of functional components, and then attached to an element in the render() function. Here is a bare-bones example where we are creating a ref and attaching it to an <input> element:

class MyComponent extends React.Component {
constructor(props) {
super(props)
this.myRef = React.createRef();
}
 ...
render() {
return (
<>
<input
name="email"
onChange={this.onChange}
ref={this.myRef}
type="text"
</>
)
}
}

Refs are created using React.createRef(), and are assigned to class properties. In the above example the ref is named myRef, which is then attached to an <input> DOM element.

Once a ref is attached to an element, that element can then be accessed and modified through the ref.

Let’s add a button to our example. We can then attach an onClick handler that will utilise myRef to focus the <input> element it is attached to:

...
handleClick = () => {
this.myRef.current.focus();
}
render() {
return (
...

<button onClick={this.handleClick}>
Focus Email Input
</button>
</>
)
}

By doing this we are in fact changing the state of an input element without any React state updates. This makes sense in the case of focussing an <input>— we wouldn’t want to re-render elements every time we focus / blur an input. There are a few more cases where refs make sense; we will visit those further down the article.

You may have noticed the current property of myRef; current refers to the element the ref is currently attached to, and is used extensively to access and modify our attached element. In fact, if we expand our example further by logging myRef in the console, we will see that the current property is indeed the only property available:

componentDidMount = () => {
   // myRef only has a current property
console.log(this.myRef);
   // myRef.current is what we are interested in
console.log(this.myRef.current);
   // focus our input automatically when component mounts
this.myRef.current.focus();
}

At the componentDidMount lifecycle stage, myRef.current will as expected be assigned to our <input> element; componentDidMount is generally a safe place to process some initial setup with refs, e.g. focussing the first input field in a form as it is mounted on stage.

What if we change componentDidMount to componentWillMount? Well, myRef will be null at this stage of the component lifecycle. Refs will be initialised after the component mounts, whereby the elements we are attaching refs to need to be mounted (or rendered) on the stage. Therefore, componentWillMount is not suitable for accessing refs.

Where refs can be attached

Refs are quite extensible in that they can be attached to DOM elements and React class components, but not to functional components. The functionalities and data we have access to depend on where you attach the ref:

  • Referencing a DOM element gives us access to its attributes
  • Referencing a class component gives us access to that component’s props, state, methods, and it’s entire prototype.

We cannot attach refs to functional components in React currently. In the following example, myRef will not be recognised and will cause an error:

// this will fail and throw an error
function FunctionComponent() {
return <input />;
}
class Parent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}

render() {
return (
<FunctionComponent ref={this.myRef} />
);
}
}

Your app will not compile if this scenario occurs, ensuring the error will not leak into a final build.

Note: To get around this limitation we can introduce ref forwarding boilerplace around functional components. We will visit this further down the article.

However, we can define refs within functional components

What we can do with functional components however is define the ref inside it, and then attach it to either a DOM element or class component. The below example is perfectly valid:

// class component will accept refs
class MyInput extends React.Component {
...
}
// ref is defined within functional component
function FunctionComponent() {
let myRef = React.createRef();
   function handleClick() {
...
}
   return (
<MyInput
handleClick={this.handlClick}
ref={this.myRef}
/>
);
}

If you see an opportunity to pass refs into a functional component, it may well be worth converting it to a class component in order to support them.

Using refs conditionally with callback refs

We can also pass functions into an element’s ref attribute, instead of a ref object — these types of refs are called callback refs. The idea is that doing so gives us more freedom related to when to set and unset refs.

The referenced element or component will be supplied as an argument to the function we are embedding within theref attribute. From here we have more control over how the ref will be set. Consider the following:

class MyComponent extends React.Component {

constructor(props) {
super(props)
this.myInput = null;
}
  focusMyInput = () => {
if (this.myInput) this.myInput.focus();
};
  setMyInputRef = element => {
this.myInput = element;
};
  componentDidMount() {
this.focusMyInput();
}
  render() {
return (
<input
name="email"
onChange={this.onChange}
ref={this.setMyInputRef}
type="text"
)
}
}

Instead of defining a ref in the constructor, we have now defined an empty myInput property. myInput will not refer to a DOM element until setMyInputRef() is called, which is done so upon the rendering of the component. Concretely:

  • The component renders and setMyInputRef() is called from within the ref attribute of the <input> element.
  • setMyInputRef() now defines the myInput class property with a full reference to the element
  • focusMyInput() can now focus the element in question.

This is seen as a more favourable approach to initialising refs, whereby the ref itself will only exist when the element in question exists on stage.

More Ref use cases

So where can refs be used apart from focusing an input? Let’s briefly go over some use cases before moving on to ref forwarding:

  • Incrementing / Decrementing input values: Attaching a ref to an <input> field in a similar fashion as the example above and creating a onClick handler that increments or decrements the value of the input. We can access the value of an input ref with:
incrementValue = () => {
this.myRef.current.value++;
}
render() {
   <input 
type="text"
ref={this.myRef}
value="0"
/>
  <button onClick={this.incremenetInput}>
Increment Input Value
</button>
}
  • Getting input values: Perhaps an input value needs to be referenced and included in a string or label — refs provide the bridge to fetch the current value from an input element. Similarly, refs can also read whether a checkbox has been checked, or which radio button has been selected; other elements on your stage could then be modified based on these values, without relying on state.
  • Selecting or cycling through form values: Having an array of valid form values and clicking next or previous buttons to update the selected value. Such an update does not warrant a state update. Selecting text is also a good use case for refs, whereby the ref would manage the currently selected or highlighted value.
  • Media playback: A React based music or video player could utilise refs to manage its current state — play / pause, or sweeping through the playback timeline. Again, these updates do not need to be state managed.
  • Transitions and keyframe animations: If you wanted to trigger an animation or transition on an element, refs can be used to do so. This is particularly useful when one element needs to trigger a style update for a separate element, which could also be nested within another component.

In some cases a ref will need to be attached to an element from within another component — this is when forwarding refs come into play. Let’s see how ref forwarding is integrated next.

Forwarding Refs

Ref forwarding is a technique to automatically pass a ref to a child component, allowing the parent component to access that child component’s element and read or modify it in some way.

React provide us with extra boilerplate specifically for ref forwarding whereby we wrap a component with React.forwardRef(). Let’s take a look at how it is used:

//handling ref forwarding to MyInput component
const MyInput = React.forwardRef((props, ref) => {
return(<input name={props.name} ref={ref} />);
});

// we can now pass a ref down into MyInput from a parent component
const MyComponent = () => {
let ref = React.createRef();
   return (
<MyInput
name="email"
ref={ref}
/>
);
}

In the above example we are using React.forwardRef(), which provides us 2 parameters — the props of the component we are wrapping, as well as an additional ref object specifically for the ref we are forwarding.

Had we not wrapped our component with React.forwardRef(), the component itself would be the thing we are referencing. This may be indeed what you intend to do, but in order to forward the ref down, React.forwardRef() is needed.

Note: The reason we require this additional boilerplate is that a ref JSX attribute is not treated as a prop. Like key, ref has specific uses within React. These can be looked at as reserved keywords for specific React features.

We can use React.forwardRef() with class components too. Doing so resembles more of a HOC setup than simply wrapping our component. Take a look at the following example, where we pass our forwarded ref down to the wrapped class component as an innerRef prop:

// we will be passing our ref down to this component, to be used for our input element
class WrappedComponent extends Component {
render() {
return (
<input
type="text"
name={this.props.name}
ref={this.props.innerRef}
/>
)
}
}
// we are wrapping our wrapped component with forwardRef, providing the ref
const MyInput = React.forwardRef((props, ref) => {
return (<WrappedComponent innerRef={ref} {...props} />);
});
export default MyInput;

This is slightly more boilerplate than before; we are opting for the innerRef prop to pass the ref down to <WrappedComponent />. From here we can go ahead and pass a ref down from a parent component into <MyInput />:

import { MyInput } from './MyInput';
const MyComponent = () => {
let ref = React.createRef();
   return (
<MyInput
name="email"
ref={ref}
/>
);
}

This brings us onto the use case of defining HOCs specifically for injecting forwarded refs into component DOM.

Using Forwarding Refs with HOCs

To expand the HOC concept further, let’s explore how we can wrap a component with a HOC that will automatically forward a ref to a wrapped component that did not originally have ref support. Concretely, what we expect here is:

  • To be able to pass a ref into a component wrapped in the HOC via a forwardedRef prop. We will name the HOC withRef.
  • The HOC to take a ref and forward it to the wrapped component
  • The wrapped component to attach the ref to a child element
// define the withRef HOC
export function withRef(WrappedComponent) {
class WithRef extends React.Component {
render() {
const {forwardedRef, ...props} = this.props;

return(
<WrappedComponent
ref={forwardedRef}
{...props}
/>);
}
}
   return React.forwardRef((props, ref) => {
return <
WithRef {...props} forwardedRef={ref} />;
});
}

Now we can simply wrap our <MyInput />component with withRef() to support ref forwarding, supplying a non-ref and ref-supported version of the component:

...
export const MyInput = (props) => (
<input
name={props.name}
type="text"
/>
);
export const MyInputWithRef = withRef(MyInput);

We are now able to import <MyInputWithRef /> into any component, which will now route a ref to the child element with the withRef() HOC behind the scenes:

import { MyInputWithRef } from './MyInput';
const MyComponent = () => {
let ref = React.createRef();
return (
<MyInputWithRef
name="email"
forwardedRef={ref}
/>
);
}

To learn more about HOCs and other use cases that can be applied to them, check out my article dedicated on the subject:

And that wraps up this talk on refs in React. Use them where appropriate — but not as a replacement to state where state is warranted. Using refs effectively will indeed simplify your component logic and provide some nice UI feedback for end users; keep them in mind when developing around forms in particularly.