React  

useRef: The React Hook You’re Sleeping On

Hooks: Why You Need to Know Them?

Before diving into useRef, let’s clear the air on hooks.

Hooks are functions that we use to handle things like data (state), actions that run after the component shows up (like fetching data), and directly working with DOM elements.

What’s the point, though?

Hooks simplify your components. You don’t need to extend React.Component or juggle lifecycles. Instead, you call functions like useState, useEffect, and yes, useRef, to get things done.

When do you use them?

Whenever you’re dealing with stateful logic, side effects, or need to interact with the DOM (spoiler alert: that’s where useRef shines).

The Problem We’re Solving

Picture this: You’re building a form, and you want to focus on an input when the user clicks a button. Or maybe you’ve got an expensive DOM element that you don’t want to rerender needlessly. That’s when you reach for useRef.

So, What Is useRef?

Technically, useRef holds a reference to an HTML element. useRef is a hook that gives you a persistent, mutable value that sticks around between renders. React technically keeps this thing alive no matter how many times I rerender.

Syntax

const h1Ref = useRef<HTMLHeadingElement>(null);

A Simple Example

import {useEffect, useRef, type ReactNode} from 'react';

export default function UseRefDemo() {
    const h1Ref = useRef<HTMLHeadingElement>(null);

    useEffect(() => {
        console.log("with useRef:", h1Ref.current?.textContent)
    }, [])

    return (
        <h1 ref={h1Ref}>This is h1 data!</h1>
    )
};
  • h1Ref is created with the useRef method, which is linked to the <h1> element using the ref prop.
  • Inside useEffect, we log the text content of the element to the console.

What’s useEffect doing?

  • useEffect runs after the component mounts (mount: The component is created and added to the DOM).
  • In this example, it’s a good place to read the DOM because the <h1> has already been added to the page.

Two Core Uses of useRef

1. Accessing DOM Elements Directly

In the following example, we’re using useRef to grab references to the <input> and <button> elements. When the page loads, the button is automatically focused. When you click the button, it focuses the input.

import {useRef, useEffect} from 'react';

export default function(){
    const inputRef = useRef<HTMLInputElement>(null);
    const buttonRef = useRef<HTMLButtonElement>(null);

    function clickHandler(){
        console.log(inputRef.current?.placeholder);
        inputRef.current?.focus();
    }

    useEffect(() => {
        buttonRef.current?.focus();}, 
        []);

    return (
        <>
            <input placeholder="Click button to focus on me" ref={inputRef}/>
            <button onClick={clickHandler} ref={buttonRef}>click me!</button>
        </>
    );
} 
  • useRef is creating a persistent object that doesn’t change across re-renders.
  • We’re giving that object to the ref attribute of the <input> and <button>.
  • When React renders the page, it automatically sets inputRef.current and buttonRef.current to the actual DOM nodes. Here current is a property on that object.
  • Now, in the rest of the code (clickHandler, useEffect), we can directly access the real DOM elements through .current.

That’s why inputRef.current?.focus() works, it’s talking straight to the real <input> element in the DOM. No rerenders or state updates needed.

inputRef.current?.focus()

 

2. Storing a Mutable Value

Sometimes, in a React component, you want to store a value that doesn’t get reset when the component re-renders. For example, you might want to:

  • Keep track of how many times a button has been clicked, without updating the UI.
  • Store a reference to a timer ID you created with setInterval or setTimeout.
  • Hold on to some previous state or calculation for debugging or analytics without showing it in the UI.
import { useRef, useState } from "react";

export default function () {
  const [show, setShow] = useState(true);
  const countRef = useRef(0);
  let num = 0;

  function clickHandler() {
    countRef.current += 1;
    num += 1;
    console.log("Ref:", countRef.current, "Plain:", num);
  }

  return (
    <>
      <button onClick={clickHandler}>Click me</button>
      <button onClick={() => setShow(!show)}>Force Re-render</button>
    </>
  );
}

This example shows how useRef helps hold a value across re-renders, while a plain variable (let num) does not.

  • Every time you click “Click me" button, both countRef.current and num get incremented and logged.
  • Clicking “Force Re-render” toggles the state, forcing React to re-run the entire function.
  • On re-render
    • num gets reset to 0 because it’s a local variable inside the component.
    • countRef.current stays at its last value because React keeps it alive across re-renders.
      countRef.current

Very Important Node: Avoid overusing it.

If you find yourself using refs for data that’s supposed to update the UI, you’re probably using it wrong. Reach for useState instead.