DEV Community

Rafael Leitão
Rafael Leitão

Posted on • Updated on

Using refs in React functional components (part 1) - useRef + callback ref

Hello everyone! 👋

Lately, I have been working a bit with refs in functional components and decided to go beyond the surface and dig a little deeper into it. Also, I decided to start writting as a way to improve my knowledge since you don't really understand something until you explain it.

That's how the idea of this series came! It won't be any complete guide of the Ref API but rather an overview of it based on what I understood while studying it to be more confident when using it in the future.

Since these are my first articles any feedback will be valuable. Hopefully, it will be helpful to you too.

If you want to check, I also put the code for these examples on github.

Without further ado, let's go!

1. What are refs?

Refs are simply references to anything, like a DOM node, Javascript value, etc. To create a ref in a functional component we use the useRef() hook which returns a mutable object with a .current property set to the initialValue we passed to the hook.

const ref = useRef(null); // ref => { current: null }
Enter fullscreen mode Exit fullscreen mode

This returned object will persist for the full lifetime of the component. Thus, throughout all of its re-rendering and until it unmounts.

There are basically two use cases for refs in React:

  • Accessing underlying DOM nodes or React Elements
  • Creating mutable instance-like variables for functional components

In the following sections and next posts I will try to cover some use cases with examples of common scenarios.

2. Accessing DOM nodes in the same React component

To create a reference to a DOM node in a component we can do it either using the useRef() hook, which for most cases is the easier and best approach, or using the callback ref pattern which gives you more control when refs are set and unset.

Let’s see how they compare in an example where there are two buttons, one which sets focus on the input and another which logs the value the user typed in the input.

2.1 useRef()

import React, { useRef } from 'react';

const SimpleRef = () => {
    const inputRef = useRef<HTMLInputElement>(null);

    const onClick = () => {
        console.log('INPUT VALUE: ', inputRef.current?.value);
    }

    const onClickFocus = () => {
        console.log('Focus input');
        inputRef.current?.focus();
    }

    return (
        <div>
            <input ref={inputRef} />
            <button onClick={onClick}>Log value</button>
            <button onClick={onClickFocus}>Focus on input</button>
        </div>
    );
};
Enter fullscreen mode Exit fullscreen mode

The useRef<HTMLInputElement>(null) returns an { current: null } object initially since we provided null as the initialValue. After we associate it to the <input>, with its ref attribute, we can access the HTMLInputElement and its properties through the .current property of the ref.

With that, when the user clicks the first button we log the input value the user typed and when he/she clicks in the second button we call the focus() method from the <input> element.

Since in this project I am using Typescript we have to set the type of the ref we are storing. As we are putting the ref on an <input>, we define it as a HTMLInputElement and use the optional chaining to prevent an error while accessing the properties of the ref.

2.2 Callback Ref

This is another way React supports to set refs. Instead of passing a ref attribute created by useRef(), you pass a function. As stated in the docs, the function receives the React component instance or HTML DOM element as its argument, which can be stored and accessed elsewhere.

There is a small difference when creating the same example with a callback ref .

const SimpleCallbackRef = () => {
    let inputRef: HTMLInputElement | null;

    const onClick = () => {
        console.log('INPUT VALUE: ', inputRef?.value);
    }

    const onFocusClick = () => {
        console.log('Focus input');
        inputRef?.focus();
    }
    console.log('Rendering')
    return (
        <div>
            <input ref={node => { inputRef = node; }} />
            <button onClick={onClick}>Log value</button>
            <button onClick={onFocusClick}>Focus on input</button>
        </div>
    );
};
Enter fullscreen mode Exit fullscreen mode

We simply set the ref attribute in the <input> with a function instead of ref attribute created by useRef(). This function receives the DOM node and assigns it to the inputRef we declared before. Since we didn't create a ref with useRef the inputRef variable stores the DOM element itself then we don't need to access the .current property, as you can see in the onClick and onFocusClick functions.

However, note that we start by setting the type of inputRef as either a HTMLInputElement or null.

Why was that? This is due to a caveat when using callback ref. As stated in the docs: when it’s defined as an inline function, it will be called twice on updates, first with null and then again with the DOM element.

So Typescript warns that the inputRef variable can be null(since the node also can be) then after typing like this Typescript won't complain.
To deal with this caveat, in this example, we can either do this or ensure that we will only assign the node to the inputRef when the node is valid:

let inputRef: HTMLInputElement;
// ... the same code
<input ref={node => { 
    console.log('Attaching node: ', node)
    if (node) { // with this we know node is not null or undefined
        inputRef = node;
    }
}} />
Enter fullscreen mode Exit fullscreen mode

This example was only made to illustrate the difference between how to use the callback ref and useRef. In such a simple case, using callback ref only gives us unnecessary work so I would go with useRef().

2.3 The callback ref pattern caveat

Still on this caveat and how to deal with it. Getting it straight from the docs:

If the ref callback is defined as an inline function, it will get called twice during updates, first with null and then again with the DOM element. This is because a new instance of the function is created with each render, so React needs to clear the old ref and set up the new one. You can avoid this by defining the ref callback as a bound method on the class, but note that it shouldn’t matter in most cases.

To illustrate this callback ref caveat better, see the example below:

import React, { useState } from 'react';

const SimpleCallbackRefRerender = () => {
    let inputRef: HTMLInputElement;
    const [count, setCount] = useState(0);

    const onClick = () => {
        console.log('INPUT VALUE: ', inputRef?.value);
    }

    const onFocusClick = () => {
        console.log('Focus input');
        inputRef?.focus();
    }

    const onRerenderClick = () => {
        console.log('Clicked to re-render');
        setCount(count+1);
    }

    return (
        <div>
            <input ref={node => { 
                console.log('Attached node: ', node)
                if (node) {
                    inputRef = node;
                }
             }} />
            <button onClick={onClick}>Log value</button>
            <button onClick={onFocusClick}>Focus on input</button>
            <button onClick={onRerenderClick}>Re-render count {count}</button>
        </div>
    );
};
Enter fullscreen mode Exit fullscreen mode

Alt Text

As you can see in the logs, when first rendering, the callback ref had the HTMLInputElement node to be passed to the ref attribute of the <input>. However, when clicking on the button to re-render, the node was first null and then it was the actual element again.

This happens because when the component is being re-rendered it first unmounts then React calls the callback ref passing null to it to clear the old ref and when it mounts again then React calls the callback ref with the DOM element. To deal with that, in the callback ref we can check whether the node is not null/undefined and then assign to the inputRef variable, as we did.

3. Accessing dynamically added DOM elements

Cool, I got that! But why am I going to use callback ref?
Well, even though there is the useRef() hook which covers most of the common cases we would need for a ref, the callback ref pattern provides us with a more powerful way to have control for cases when a child is being added or removed dynamically, does not have the same lifetime as the parent or you need to perform any effect when a ref has been mounted.

Let’s consider a simple example where part of a form only appears when the user clicks in the first button and we when it happens we want the newly shown input to be focused.

import React, { useState, useRef } from 'react';

const CallbackRefDynamicChild = () => {
    const inputRef = useRef<HTMLInputElement>(null);
    const [visible, setVisibility] = useState(false);

    const onClick = () => {
        console.log('INPUT VALUE: ', inputRef.current?.value);
        setVisibility(true);
    }

    const onFocusClick = () => {
        console.log('Focus on first input');
        inputRef.current?.focus();
    }

    const callbackRef = (node: HTMLInputElement) => {
        console.log('Attached node: ', node);
        if(node) {
            node.focus();
        }
    }

    console.log('Rendering: ', inputRef);
    return (
        <div>
            <input ref={inputRef} />
            <button onClick={onClick}>Unlock next input</button>
            {visible && (
                <>
                <input ref={callbackRef} />
                <button onClick={onFocusClick}>Focus on first input</button>
                </>
            )}
        </div>
    );
};
Enter fullscreen mode Exit fullscreen mode

Since the second input is added dynamically, when the state changes and the visible variable is set to true, the best approach for this is using the callback ref.

The useRef doesn’t notify you when its content changes. Mutating the .current property doesn’t cause a re-render. Therefore, to run any effect when React attaches or detaches a ref to a DOM node, we would need to use the callback ref.

With callback ref, when the second input shows up and the ref is attached to the <input>, the callbackRef function is called with the HTMLInputElement. Then if the node is not null/undefined we call the focus() method to achieve what we wanted.

4. Conclusion

In this first part of the series, we covered possible ways to use refs in functional components for the case where we want to access DOM nodes in the same component.

In the next posts we will see how to use refs to access other React components and also to have an instance-like variable in functional components.

If you've came this far, I would really appreciate any feedback or comments pointing any corrections you would suggest. Hopefully this will be helpful to you :)

5. References

This series would not be possible without other articles from awesome developers out there. If you want check what helped my learning, click on the links below:

https://moduscreate.com/blog/everything-you-need-to-know-about-refs-in-react/
https://blog.logrocket.com/how-to-use-react-createref-ea014ad09dba/
https://www.robinwieruch.de/react-ref
https://medium.com/trabe/react-useref-hook-b6c9d39e2022
https://elfi-y.medium.com/react-callback-refs-a-4bd2da317269
https://linguinecode.com/post/how-to-use-react-useref-with-typescript
https://reactjs.org/docs/refs-and-the-dom.html

Top comments (4)

Collapse
 
nghiepit profile image
Nghiệp

Just Callback Ref with useCallback
reactjs.org/docs/hooks-faq.html#ho...

Collapse
 
jscheinhorn profile image
jscheinhorn

good to note that createRef doesn't work for functional components (as noted in the second reference link)

Collapse
 
alexdewaal66 profile image
alexdewaal66

Why is secRef defined, it seems it's never used?

Collapse
 
selfprogrammed00 profile image
selfprogrammed00

Yes I was thinking the same, I think it's a typo. secref should basically be callbackRef.