Hooks were introduced to React in late 2018/early 2019 when React 16.8 was released. They’re a way of getting away from traditional class components by replacing them with functional components but they still allow us to leverage React’s life cycle methods as well as component state. One of the main motivating factor behind hooks was to make code more readable and easier to manage.

The two most common hooks we use when developing a React app are without a doubt useState and useEffect. But there are several other “out of the box” React provides to us, one being useRef. Let’s explore what the useRef hook is, what it does, and perhaps explore a few real world use cases for it.

What is the useRef hook/What does it do?

Simply put, the useRef hook returns a mutable (Can be modified) ref object that has a .current property that is set to the value of the argument that was just passed in. The returned object persists between component re-renders.

This is somewhat similar to the useState hook in which the initial value of the variable assigned by the hook is derived from the argument passed in to useState. One of the main differences between the two however is that the useRef hook does not cause a component re-render. This means it is an excellent choice for when you need to keep track of data but do not want to cause a potentially resource-intensive/expensive re-render. You can do this all “behind the scenes” with useRef!

Let’s see some “real world” examples of the useRef hook in action. Check out the code example below with comments explaining what’s going on for some more insight in to how we can use useRef within our React apps:

import * as React from "react";

const App = () => {
  const counter = React.useRef(0);
  const [rerender, setRerender] = React.useState(false);

  const updateCounter = () => {
    console.log(typeof counter); //sanity check - Yup - it's an object
    console.log(counter); //let's see what's in the counter variable before updating it - it's an object with a key named 'current' set to 0
    counter.current++;
    console.log(counter.current, "Current value of counter");
    //console.log will print the incremented value of counter.current to the console
  };

  return (
    <>
      <h3>Example showing no re-renders</h3>
      <p>Current value: {counter.current}</p>
      {/* here counter.current will NOT update because it does not cause a component re-render */}
      <button onClick={updateCounter}>Increment Counter</button>
      <h3>
        Example showing how to use the useRef element in conjunction with DOM
        elements
      </h3>
      <InputComponent />
      <h3>Force a re-render</h3>
      {*/ let's force a re-render to prove that behind the scenes React is still keeping track of the counter's value but just not re-rendering the BOM */}
      <button onClick={() => setRerender(!rerender)}>Force re-render</button>
    </>
  );
};

const InputComponent = () => {
  let inputElement = React.useRef(null);

  const handleClick = () => {
    inputElement.current.focus();
  };

  return (
    <>
      <input type="text" ref={inputElement} />
      <button onClick={handleClick}>Click to focus!</button>
    </>
  );
};

export default App;

Bonus points: Thanks to JavaScript hoisting we can define a function after it’s been been called and it will not throw an error. Notice how the InputComponent function (component) is defined AFTER it was used within the App function (component).