← Back to blog

useRef vs useState: The Ultimate React Hooks Showdown

A no-BS comparison of useRef and useState — what each hook is for, their real-world trade-offs, and dead-simple rules for picking the right one.

useRef vs useState

By definition, useRef is a built-in feature that provides a way to store a mutable value that persists across component re-renders without triggering a re-render. Now imagine a situation where you’re tracking the previous values of a counter in real-time, and you want it fast and you want it to happen smoothly without your UI flickering on every update.

Enter our mutant superhero: useRef, whose mutable persistence for DOM refs, timers, or “shadow state” is perfectly made for situations like these.

useState is the exact opposite, React’s inbuilt reactive rockstar, that craves attention with every update, forcing a full re-render without actually thinking why to. Perfect for visible changes like button toggles or form values.

While it is perfect for certain situations, using it in any other situation that requires frequent updating or extensive calculations can cause multiple re-renders, which ultimately can bog down your app and result in chaos and confusion.

In this post, we will discuss what useRef and useState are, compare their strengths head-to-head in a clear showdown, and arm you with dead-simple rules for where to use each without making you feel dumb.

Think of it as your no-BS React hook cheat sheet for:

useState: The Drama Queen Who Steals Every Spotlight

useState is React’s built-in hook that craves attention with every update, forcing a re-render without actually thinking about whether it’s required or not. A state generally refers to a piece of data or a certain set of properties that needs to be tracked in an application. For example, if you want to track the number of times a button is clicked, a state variable will help you do that.

Here’s an example for the same:

import { useState } from 'react';

function Counter() {
  // Declare a state variable 'count' and a function 'setCount' to update it
  const [count, setCount] = useState(0);

  // Function to increase count when the button is clicked
  const handleClick = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <h2>Clicked: {count} times</h2>
      <button onClick={handleClick}>Click Me</button>
    </div>
  );
}

If you notice, every time the button is clicked, the state value changes, and it is not displayed immediately since React has to re-render the entire component first to show the updated value.

That’s one of the biggest benefits (and disadvantages) of using state. While it does ensure you don’t miss any changes (be it a state change or a UI change) and automatically reflects the latest data, unnecessary updates can hurt your app’s performance.

In short, useState is perfect for data that’s visible to a user in the frontend, like form values, toggles, user inputs, or counters (as we saw above).

While you may think of using useState for updating every other state in your React app, all is not that easy. You still need to know the major disadvantages of useState before you start using it:

Although using a library won’t harm you, it will ultimately increase your coding time to implement a simple, thorough change.

useRef: The Silent Ninja That Works Behind the Scenes

Now it’s time for our next champion, useRef, the complete opposite of useState. While useState unnecessarily renders everything, useRef handles everything subtly and quietly.

useRef is a React hook that creates a mutable object with a .current property. The .current property helps you store any value — numbers, strings, DOM elements, objects, functions, you name it, useRef has got it. That’s not it, you can also change .current anytime, and it won’t unnecessarily re-render your component. A quick example to sum up how it works:

import { useRef } from 'react';

function SimpleRef() {
  const myRef = useRef(0);

  const updateRef = () => {
    myRef.current = myRef.current + 1; // Change it!
    console.log('Ref value:', myRef.current);
  };

  return (
    <div>
      <button onClick={updateRef}>
        Update Ref (No re-render!)
      </button>
      <p>Component stays calm</p>
    </div>
  );
}

Now, before you go ahead and use useRef for everything, here are some real examples of what you can store in useRef without going crazy:

Use CaseWhat to StoreWhy useRef?
DOM Elements<input ref={myRef}>myRef.current.focus()
Previous ValuesprevCountRef.current = countTrack changes silently
TimersintervalRef.current = setInterval(...)Clean up without re-renders
Canvas ContextcanvasRef.current.getContext('2d')Heavy drawing operations
FlagsisInitializedRef.current = trueInternal component state

While it may seem like useRef can be a perfect solution every time you encounter one of these use cases, that’s not the case. Similar to useState, useRef also has its own set of disadvantages, which you should keep in mind before implementing it.

Master useRef vs useState Like a React Pro

And there you have it, the ultimate useRef vs useState showdown!

The simple truth:

  1. Does this value need to show up in the JSX or change the UI? Yes, then pick useState and move on.
  2. Does this value need to be stored or accessed in the background (like a DOM node, timer ID, previous value, or a simple flag) without triggering a re-render? Then pick useRef and move on.

Neither hook is “better” — each has its pros and cons. Using them according to your use case is a better choice than picking one blindly. Use useState for reactivity, useRef for efficiency, and combine them for React mastery. Using them together will ultimately result in a smoother, more efficient app.

If you liked this post, you might also enjoy Zustand vs Redux: Which React State Manager Wins in 2026.