React Hooks review #7: useRef

This is chapter seven of the React Hooks series. The goal was simple: summarize all the eleven official React hooks:

useRef

A mutable object whose .current property is initialized to the passed argument. The returned object will persist for the full lifetime of the component.

React.js

Syntax:

const refContainer = useRef(initialValue);
  • initialValue: The value stored between re-renderers.
  • refContainer: Mutable ref object structured as {current: initialValue}.

DOM reminder and react.ref

On the prehistoric Vanilla days, we got and manipulated DOM elements with good old “document.getElementById” or later on “document.querySelector“. If we wanted to check or modify an element we used something like:

 <script>
     function changeMe(e,newText){
        e.innerText = newText;
     }
</script>
<div onclick="changeMe(this,'changed')">Loading...</div>

But once we got to the era of React, the simple getElementBy... became an-anty-pattern, and for good reasons. This is why React created the ref: “a way to access DOM nodes or React elements created in the render method”.

<input type="text" ref={textInput} />

Note:

ref‘s are escape hatches for React developers, and we should try to avoid using them if possible.

A guide to React refs: useRef and createRef

useRef for the DOM

useRef is a hook created to “access a child imperatively”. That’s why it has the DOM structure of {current:someValue}, which is suitable for DOM elements.

In the most common example, the useRef creates a mutable to reference the input field. This way we can easily do DOM manipulation on the element without re-render, like setting the focus.

const SetFocus = () => {
  const textInput = React.useRef();
  const focusTextInput = () => textInput.current.focus();
  return (
    <>
      <input type="text" ref={textInput} />
      <button onClick={focusTextInput}>Focus the text input</button>
    </>
  );
}

Another example, this time copying text from one input to the other:

const CopyInputValue = ({counter}) => {
    const textInput = React.useRef();
    const disabledText = React.useRef();
    const focusTextInput = () => {
        disabledText.current.value = textInput.current.value;
    }
  return (
    <>
        <input type="text" ref={textInput} />
        <input type="text" ref={disabledText} disabled={true} />
        <button onClick={focusTextInput}>Copy test</button>
    </>
  );
}

One more, that really shows the strength of useRef. On this one, we give the parent control over its subcomponents.

const MyInput = (props)=>{
    const inputRef = React.useRef();
    const myOnChange = ()=>{
        console.log('Sub change')
        props.onBlur(inputRef.current);
    }

    return <input onBlur={myOnChange} ref={inputRef} />
}

const App = ()=>{
    const myOnBlur = (inputRef)=> { 
        inputRef.value = "Hi";
    }
    return <MyInput onBlur={myOnBlur} />
}

This option becomes nicer once used with React.forwardRef. This allows passing ref from one component to its sub-components directly, without callbacks.

const MyInput = React.forwardRef((props, ref)=>{
    return <input {...props} ref={ref} />
})

const App = ()=>{
    const myInputRef = React.useRef();
    const myOnBlur = ()=> { 
        myInputRef.current.value = "Hi";
    }
    return <MyInput onBlur={myOnBlur} ref={myInputRef} />
}

Trying to pass the ref without forwardRef will block the ref, and throw a console error: “Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

useRef as state without re-rendering

Since useRef returns a persistable object with the format of {current:someValue}, It is similar to useState with the same object type.

This example had two similar renrening counters. One with useState, and the other with useRef:

const Aka = () => {
    const count = React.useRef(0);
    count.current += 1;

    const [myState,] = React.useState({current:0});
    myState.current += 1;

    return (
      <div>
        <p>I have rendered {count.current} times.</p>
        <p>I have rendered {myState.current} times.</p>
      </div>
    );
  }

As useState does not re-render if an inner value had changed, if useRef.current value had changed, the component will not re-render. This explains the second “official” usage for useRef.

Is there something like instance variables?

Yes! The useRef() Hook isn’t just for DOM refs.

Hooks FAQ

Or:

an object in which mutable information can be stored without triggering a re-render… particularly useful for working with DOM elements.

Understanding useRef

Usually, when you make something, you find out you can use it for different use cases. But this time, it seems as someone confused it as an official purpose of the element. But maybe I’m wrong. Anyway…

Since the current value does not change, it can be used to store values that don’t suppose to trigger a DOM change or re-render something

const StoringHiddenData = ({counter}) => {
    const ref = React.useRef("Hi");
    const flick = () => {
        console.log(ref); 
        ref.current = "To Me";
    }
    return <p onClick={flick}>
        Current value: {ref.current}
    </p>
} 

In this example, clicking the element will change the rev value. But until the parent component will re-render the StoringHiddenData (change the counter in the prop), the component will continue to show “Hi”. Once the counter changes, the inner text will show the updated “To Me” ref.current value.

If for some reason, you want to force a re-render, you can use the empty useState trick. But if you find that you need to do it, you probably weren’t supposed to use useRef in the first place.

const Aka = ({counter}) => {
    const [, forceUpdate] = React.useState({});

    const ref = React.useRef("Hi");
    const flick = () => {
        console.log(ref); 
        forceUpdate({});
        ref.current = "To Me";
    }
    return <p onClick={flick}>
        Current value: {ref.current}
    </p>
}

Note, unlike useState, you can’t add additional properties to the useRef hook. So this example will throw an error:

const Aka = ({counter}) => {
    const ref = React.useRef("Hi");
    const flick = () => {
        ref.someOtherProp = "BlaBla";
    }
    return <p onClick={flick}>
        Current value: {ref.current}
    </p>
}

Conclusion

useRef looks like something meant only for ref usage. You can use it to store values when you don’t want to re-rendering the component. But useState can be used in the same way, with less unexplainable limitations. But since React claims differently, I’m probably just missing something.