NIXX/DEVv1.14.0
ArticlesFavorites
Sign In
Sign In
Articles

Welcome to our blog

A curated collection of insightful articles, practical guides, and expert tips designed to simplify your workflow

Cover image for: The Ultimate Guide to useRef in React: Beyond Just DOM References
August 19, 20256 MIN READ min readBy ℵi✗✗

The Ultimate Guide to useRef in React: Beyond Just DOM References

`useRef` is one of React’s most versatile hooks — but most developers only use it for grabbing DOM nodes. In this guide, we’ll explore practical, real-world ways to use `useRef` that go far beyond the basics.

webdevreactjavascriptuseRef
ℵi✗✗

ℵi✗✗

Full-Stack Developer

Passionate about building tools and sharing knowledge with the developer community.

Was this helpful?

Popular Posts

  • NixOS vs. Arch Linux: Which One Belongs in Your Dev Setup?

    NixOS vs. Arch Linux: Which One Belongs in Your Dev Setup?

    5 MIN READ min read

  • How to Enable HTTPS on Localhost in Under 2 Minutes

    How to Enable HTTPS on Localhost in Under 2 Minutes

    3 MIN READ min read

  • Migrating from Create React App (CRA) to Vite: A Step-by-Step Guide

    Migrating from Create React App (CRA) to Vite: A Step-by-Step Guide

    4 MIN READ min read

  • Array Destructuring in PHP: A Practical Guide for Modern Developers

    Array Destructuring in PHP: A Practical Guide for Modern Developers

    5 MIN READ min read

Recommended Products

  • Apple iPad (7th Gen)

    Apple iPad (7th Gen)

    4.3
  • Fitbit Versa 4

    Fitbit Versa 4

    4.3
  • JBL Flip 6

    JBL Flip 6

    4.8
  • Dell 24 Monitor — SE2425HM Full HD

    Dell 24 Monitor — SE2425HM Full HD

    4.7

May contain affiliate links

Topics

webdev33productivity16cybersecurity12javascript11automation9guide8react7typescript7php6tutorial6freelancing5github actions5privacy5how to4Node.js4
+111 more topics →
🇺🇸USD ACCOUNTOpen a free US-based USD accountReceive & save in USD — powered by ClevaSponsoredInterserver Hosting#1 VALUEAffordable, reliable hosting from $2.50/mo99.9% uptimeSponsored

Most React developers first encounter useRef when they need to focus an input or measure a DOM element. It works for that, but the hook's underlying behavior makes it useful in a much wider range of situations. useRef returns a mutable container whose .current value persists across renders without triggering re-renders when it changes. That combination of mutability and persistence solves a category of problems that neither useState nor useCallback handles cleanly.

This guide covers the foundational behavior of useRef, the DOM reference use case, and four patterns that apply it to non-DOM problems: tracking previous values, storing timer IDs, avoiding stale closures in event callbacks, and caching expensive computed values.

What this covers:

  • How useRef works and how it differs from useState

  • DOM references and direct element manipulation

  • Tracking previous prop or state values

  • Storing timer and interval IDs

  • Stable event callbacks to avoid stale closures

  • Caching expensive values without triggering re-renders

  • When to use useRef vs. useState vs. useMemo


How useRef Works

useRef returns a plain object with a single .current property, initialized to the value passed as the argument:

const ref = useRef(0);
console.log(ref.current); // 0

Two properties distinguish it from state:

Mutability without re-renders. Assigning to ref.current does not notify React and does not cause the component to re-render. The change takes effect immediately in the current execution context.

Persistence across renders. The same ref object is returned on every render. Unlike a regular variable declared inside the component body, ref.current is not reset when the component re-renders.

The practical implication: useRef is the right tool when a value needs to be remembered between renders but changing it should not update the UI. When a value change should update the UI, use useState.


DOM References

The most common use case is attaching a ref to a DOM element to read properties or call methods directly:

import { useRef } from "react";

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

    function focusInput() {
        inputRef.current?.focus();
    }

    return (
        <div>
            <input ref={inputRef} type="text" placeholder="Search..." />
            <button onClick={focusInput}>Focus</button>
        </div>
    );
}

The type parameter HTMLInputElement provides TypeScript's DOM typings on inputRef.current, so calling .focus(), .select(), or reading .value is type-checked. The optional chaining (?.) handles the case where the ref is not yet attached to a mounted element.

DOM refs are also useful for: measuring element dimensions with getBoundingClientRect, triggering animations on a canvas element, integrating with third-party libraries that require a direct DOM node, and managing focus in accessible UI components.


1. Tracking the Previous Value of a Prop or State

useRef combined with useEffect produces a usePrevious hook that captures what a value was on the previous render:

import { useRef, useEffect } from "react";

function usePrevious<T>(value: T): T | undefined {
    const ref = useRef<T>();

    useEffect(() => {
        ref.current = value;
    });

    return ref.current;
}

Usage:

function PriceDisplay({ price }: { price: number }) {
    const previousPrice = usePrevious(price);

    return (
        <div>
            <p>Current: {price}</p>
            {previousPrice !== undefined && (
                <p>Previous: {previousPrice}</p>
            )}
        </div>
    );
}

The effect runs after every render and stores the current value. When the component renders next time, ref.current contains the value from the previous render, and the effect runs again to update it for the render after that.

The effect has no dependency array, which means it runs after every render. This is intentional: the goal is to always store the most recently rendered value.


2. Storing Timer and Interval IDs

Storing a timer ID in state causes a re-render when the timer starts or stops. The timer ID is not a visual concern, so useRef is the more appropriate choice:

import { useRef } from "react";

export default function DelayedAction() {
    const timerIdRef = useRef<ReturnType<typeof setTimeout> | null>(null);

    function start() {
        if (timerIdRef.current !== null) return; // prevent duplicate timers
        timerIdRef.current = setTimeout(() => {
            console.log("Action triggered");
            timerIdRef.current = null;
        }, 2000);
    }

    function cancel() {
        if (timerIdRef.current !== null) {
            clearTimeout(timerIdRef.current);
            timerIdRef.current = null;
        }
    }

    return (
        <div>
            <button onClick={start}>Start</button>
            <button onClick={cancel}>Cancel</button>
        </div>
    );
}

Using ReturnType<typeof setTimeout> as the type is more portable than number — in Node.js environments it returns a Timeout object rather than a number, and this type works correctly in both.

The same pattern applies to setInterval IDs, animation frame IDs from requestAnimationFrame, and any other external handle that needs to be cleared later.


3. Stable Event Callbacks

When a callback is passed to an effect with an empty dependency array, it captures the values from the render in which the effect ran. If the callback's closure values change on a later render, the effect still holds the original version. This is a stale closure.

One solution is to add the callback to the effect's dependency array, which re-registers the effect on every render the callback changes. For expensive setup/teardown operations (like adding and removing event listeners), this causes unnecessary re-registration.

A useRef-based stable callback avoids re-registration while keeping the callback current:

import { useRef, useEffect } from "react";

function useStableCallback<T extends (...args: unknown[]) => unknown>(
    callback: T
): T {
    const callbackRef = useRef(callback);

    useEffect(() => {
        callbackRef.current = callback;
    });

    return ((...args: Parameters<T>) =>
        callbackRef.current(...args)) as T;
}

Usage with an event listener:

function ScrollTracker({ onScroll }: { onScroll: (y: number) => void }) {
    const stableOnScroll = useStableCallback(onScroll);

    useEffect(() => {
        function handleScroll() {
            stableOnScroll(window.scrollY);
        }

        window.addEventListener("scroll", handleScroll);
        return () => window.removeEventListener("scroll", handleScroll);
    }, []); // empty array is safe — stableOnScroll always calls the latest callback

    return null;
}

The event listener is registered once. The callbackRef is updated after every render to hold the latest onScroll prop. When the scroll event fires, it calls callbackRef.current, which is always current.

React 19 introduces useEffectEvent as an official solution to this pattern. In React 18 and below, the useRef approach is the idiomatic workaround.


4. Caching Expensive Computed Values

useMemo is the standard tool for caching expensive computations and recomputing only when dependencies change. For a value that should be computed once and never recomputed, useRef initialized with the computation result is simpler:

const processedData = useRef(expensiveTransform(rawData));
// processedData.current holds the result, computed once on first render

This is appropriate when the computation truly has no dependencies that would require recomputation. If the computation depends on props or state, use useMemo instead.

A more practical case is initializing a third-party class or SDK instance once:

const editorRef = useRef<EditorInstance | null>(null);

if (editorRef.current === null) {
    editorRef.current = new EditorInstance({ /* config */ });
}

The lazy initialization pattern (if (ref.current === null)) ensures the instance is only created once, regardless of how many times the component renders.


Choosing Between useRef, useState, and useMemo

Need

Tool

Value that triggers UI update when changed

useState

Cached value recomputed when dependencies change

useMemo

Value that persists across renders without triggering re-renders

useRef

DOM element access

useRef

Timer/interval ID

useRef

Always-current callback reference

useRef

The most common mistake is reaching for useRef when useState is appropriate because the developer wants to avoid a re-render. If the UI should reflect the value, it should be state. useRef is for values that are implementation details of the component, not data that the component renders.


Key Takeaways

  • useRef returns a mutable container whose .current value persists across renders without triggering re-renders when changed.

  • DOM references are the most visible use case, but useRef is equally useful for timer IDs, previous values, stable callbacks, and one-time initialization.

  • usePrevious uses a no-dependency-array effect to capture the value from the previous render.

  • Timer and interval IDs stored in refs do not cause re-renders when the timer starts or stops.

  • The stable callback pattern keeps a callback ref current after every render, allowing event listener effects to run once while always calling the latest version of the callback.

  • Use useMemo when a cached value has dependencies that require recomputation. Use useRef when the value should be computed once and never recomputed.


Conclusion

useRef solves a specific set of problems that arize from React's render model: values that need to persist without causing re-renders, callbacks that need to be current without causing effects to re-register, and handles to external resources that need to be cleaned up. Understanding when these problems are present and reaching for useRef to address them produces components that are simpler and more efficient than approaches that route everything through state.


Using useRef for a pattern not covered here? Share it in the comments.

Topics
webdevreactjavascriptuseRef

Discussion

Join the discussion

Sign in to share your thoughts and engage with the community.

Sign In
Loading comments…

Continue Reading

More Articles

View all
Cover image for: AI for DevOps: Tools That Are Already Changing the Game
Jun 17, 20256 MIN READ min read

AI for DevOps: Tools That Are Already Changing the Game

How artificial intelligence is transforming CI/CD pipelines, monitoring, and incident response—today.

Cover image for: React Authentication with JWT: A Step-by-Step Guide
Oct 17, 20257 MIN READ min read

React Authentication with JWT: A Step-by-Step Guide

Learn how to implement secure JWT authentication in React. From login to route protection and API calls, this guide covers everything you need to know.

Cover image for: Embedding Cybersecurity in Development: Best Practices for 2025
Jul 1, 20257 MIN READ min read

Embedding Cybersecurity in Development: Best Practices for 2025

A developer-focused guide to integrating security into your workflow—covering tools, practices, and mindset shifts for 2025.

Cover image for: Best Web Hosting of 2026 (Honest Picks From Real-World Use)
Jan 1, 20267 MIN READ min read

Best Web Hosting of 2026 (Honest Picks From Real-World Use)

Choosing the right web hosting in 2026 isn't just about price. A breakdown of the best providers, focusing on reliability, performance, and support.

|Made with · © 2026|TermsPrivacy
AboutBlogContact

Free, open-source tools for developers and creators · Community driven