DEV Community

Kathirvel S
Kathirvel S

Posted on

Mastering useRef in React: The Hook That Gives React Memory Without Re-Rendering

When developers first learn React, most of the focus goes toward useState.

Because state changes update the UI.

Buttons update.

Counters increase.

Forms react instantly.

Everything feels connected.

But after building larger applications, one important question appears:

What if we need to store something inside a component, but we do NOT want React to re-render the component every time it changes?

That exact problem led to the creation of useRef.

And this is where many developers suddenly realize:

useRef is not just another hook.

It is one of React’s most important performance tools.

It allows components to:

  • Remember values across renders
  • Access real DOM elements
  • Store mutable data
  • Avoid unnecessary re-renders
  • Preserve values silently in memory
  • Handle timers, scrolling, focus, animations, and external libraries

The interesting part is:

Most developers use only a tiny portion of what useRef can actually do.

They think refs are only for focusing inputs.

But internally, refs are much more powerful than that.

In this 11th episode of “Let’s Master React Hooks Together”, we are going far beyond the basic understanding of useRef.

We will deeply understand:

  • What useRef actually is
  • Why React created it
  • The core problem it solves
  • How React stores refs internally
  • Why refs don’t trigger re-renders
  • How refs differ from state
  • Real-world use cases
  • Performance benefits
  • Hidden behaviors
  • Advanced patterns
  • Common mistakes developers make

And by the end, you’ll understand not only how to use useRef, but also how React itself thinks about refs internally.


What is useRef in React?

According to the official React definition:

useRef is a React Hook that lets you reference a value that’s not needed for rendering.

Syntax:

const ref = useRef(initialValue)
Enter fullscreen mode Exit fullscreen mode

React returns an object like this:

{
  current: initialValue
}
Enter fullscreen mode Exit fullscreen mode

Example:

const countRef = useRef(0)
Enter fullscreen mode Exit fullscreen mode

Now React creates an object internally:

{
  current: 0
}
Enter fullscreen mode Exit fullscreen mode

And the important thing is:

React keeps this same object alive between renders.

Not a copy.

Not a new object.

The exact same object.

Only the value inside .current changes.

That detail is the entire foundation of how refs work.


Why Does React Need useRef?

To understand this properly, we need to first understand how React components work internally.

Every time a component re-renders:

  • The entire component function executes again
  • Local variables are recreated
  • Function memory resets

Example:

function App() {
  let count = 0

  console.log(count)
}
Enter fullscreen mode Exit fullscreen mode

Every render creates:

count = 0
Enter fullscreen mode Exit fullscreen mode

again and again.

The variable does NOT survive renders.

This creates a huge problem.

Sometimes applications need values that must:

  • Persist between renders
  • Stay in memory
  • Avoid UI updates
  • Avoid expensive re-renders

Examples:

  • Timer IDs
  • Previous values
  • Scroll positions
  • Input references
  • Animation states
  • External library instances

Using state for these values becomes inefficient.

Because state updates trigger rendering.

And rendering is expensive when unnecessary.

This is exactly where useRef enters.

React needed a way to:

Store persistent mutable values without re-rendering the UI.

That solution became useRef.


The Core Problem useRef Solves

Imagine a large dashboard application.

You are storing a timer ID:

const [timerId, setTimerId] = useState()
Enter fullscreen mode Exit fullscreen mode

Every time the timer changes:

  • State updates
  • Component re-renders
  • Child components may re-render
  • Virtual DOM comparisons happen again

But the UI never even displays the timer ID.

So why re-render?

There is absolutely no reason.

That render becomes wasted work.

Now imagine using:

const timerRef = useRef()
Enter fullscreen mode Exit fullscreen mode

Now:

  • Value updates silently
  • No re-render happens
  • React skips unnecessary work
  • Performance improves

This is one of the biggest optimization ideas inside React.


The Real Mental Model of useRef

Most tutorials say:

“Refs store values.”

But that understanding is incomplete.

A better mental model is this:

useRef is a persistent memory container that React does not track for rendering.

That single sentence explains everything.

React knows the ref exists.

But React does NOT monitor .current for UI updates.

That means:

ref.current = 100
Enter fullscreen mode Exit fullscreen mode

does not tell React:

“Hey, render again.”

React simply ignores it.

That is why refs are extremely fast.


useState vs useRef — The Deep Difference

At surface level:

useState useRef
Triggers re-render Does NOT trigger re-render
Used for UI updates Used for mutable storage
Reactive Non-reactive
React tracks changes React ignores .current changes

But internally, the difference is much deeper.


What Happens When State Changes?

setCount(count + 1)
Enter fullscreen mode Exit fullscreen mode

React sees:

  • State changed
  • UI may need updating
  • Component must re-render

So React schedules rendering work.


What Happens When Ref Changes?

countRef.current += 1
Enter fullscreen mode Exit fullscreen mode

React sees nothing.

Because refs are mutable containers.

React does not compare .current.

So no render happens.

This is why refs are ideal for invisible internal data.


Understanding useRef Through a Counter


Counter Using State

import { useState } from "react"

function Counter() {
  const [count, setCount] = useState(0)

  const increment = () => {
    setCount(count + 1)
  }

  return (
    <div>
      <h1>{count}</h1>

      <button onClick={increment}>
        Increment
      </button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Flow:

  1. Button clicked
  2. setCount() runs
  3. State changes
  4. React schedules re-render
  5. Component executes again
  6. New UI appears on screen

State is deeply connected to rendering.

That is why UI updates happen instantly.


Counter Using Ref

import { useRef } from "react"

function Counter() {
  const countRef = useRef(0)

  const increment = () => {
    countRef.current += 1

    console.log(countRef.current)
  }

  return (
    <div>
      <button onClick={increment}>
        Increment
      </button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Flow:

  1. Button clicked
  2. .current changes
  3. React ignores the change
  4. Component does NOT re-render
  5. Value still persists internally

This is the first moment developers truly understand refs.

The value changes.

But the UI remains unchanged.

Because refs are not connected to rendering.


Why .current Exists Instead of Direct Values

A common question:

Why not simply do this?

const ref = 0
Enter fullscreen mode Exit fullscreen mode

Because React needs a stable object reference.

If React returned raw values:

  • Values would reset every render
  • References would break
  • Persistence would disappear

Instead, React creates one stable object:

{
  current: value
}
Enter fullscreen mode Exit fullscreen mode

Then React preserves that object forever during the component lifecycle.

Only .current changes.

This is why refs survive renders.


Accessing DOM Elements with useRef

One of the most famous use cases.

Example:

import { useRef } from "react"

function FocusInput() {
  const inputRef = useRef()

  const focusInput = () => {
    inputRef.current.focus()
  }

  return (
    <div>
      <input ref={inputRef} />

      <button onClick={focusInput}>
        Focus Input
      </button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Chain of what happens:

  1. React renders <input />
  2. React creates actual DOM node
  3. React stores DOM node inside inputRef.current
  4. Button clicked
  5. focusInput() executes
  6. .focus() runs on real DOM element
  7. Browser focuses the input instantly

This is direct communication with the DOM.

Without refs, React components normally stay abstracted away from real DOM access.

Refs create that bridge.


Why Direct DOM Access Matters

React normally follows declarative UI.

Meaning:

<input />
Enter fullscreen mode Exit fullscreen mode

describes what UI should look like.

But some browser features require imperative control.

Examples:

  • Focus
  • Scrolling
  • Text selection
  • Playing videos
  • Canvas drawing
  • Animations

Refs make these actions possible.


Storing Previous Values Using useRef

This is one of the smartest real-world patterns.

import { useEffect, useRef } from "react"

function Example({ value }) {
  const previousValue = useRef()

  useEffect(() => {
    previousValue.current = value
  }, [value])

  return (
    <div>
      <h2>Current: {value}</h2>
      <h2>Previous: {previousValue.current}</h2>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

How this works internally:

  1. Component renders
  2. previousValue.current initially empty
  3. useEffect runs after render
  4. Current value stored inside ref
  5. Next render happens
  6. Old value still exists inside ref
  7. Previous value becomes accessible

This pattern is extremely useful in:

  • Form comparisons
  • Analytics tracking
  • Detecting value changes
  • Undo systems
  • Animations

Managing Timers Without Re-Renders

Bad approach:

const [timerId, setTimerId] = useState()
Enter fullscreen mode Exit fullscreen mode

Every timer update triggers rendering.

Completely unnecessary.

Better approach:

const timerRef = useRef()
Enter fullscreen mode Exit fullscreen mode

Example:

timerRef.current = setInterval(() => {
  console.log("Running")
}, 1000)
Enter fullscreen mode Exit fullscreen mode

Later:

clearInterval(timerRef.current)
Enter fullscreen mode Exit fullscreen mode

Flow:

  1. Timer created
  2. ID stored in ref
  3. Ref updates silently
  4. No render triggered
  5. Timer remains accessible anytime

This is efficient React architecture.


Tracking Render Count

import { useRef, useEffect } from "react"

function App() {
  const renderCount = useRef(0)

  useEffect(() => {
    renderCount.current += 1
  })

  return (
    <h1>Renders: {renderCount.current}</h1>
  )
}
Enter fullscreen mode Exit fullscreen mode

Interesting behavior:

  • Updating ref does not re-render
  • But effect runs after every render
  • Ref silently remembers count

This becomes incredibly useful while debugging performance issues.


Solving Stale Closure Problems

One of the most advanced useRef use cases.

Problem:

setInterval(() => {
  console.log(count)
}, 1000)
Enter fullscreen mode Exit fullscreen mode

Sometimes intervals capture old state values.

Meaning:

The interval keeps reading outdated data.

Solution:

const countRef = useRef(count)

useEffect(() => {
  countRef.current = count
}, [count])
Enter fullscreen mode Exit fullscreen mode

Now interval reads:

countRef.current
Enter fullscreen mode Exit fullscreen mode

Flow:

  1. State updates
  2. Effect runs
  3. Latest value copied into ref
  4. Interval accesses ref
  5. Interval always gets newest value

This pattern is heavily used in real-time systems.


useRef Is Actually About Performance

Most people think refs are mainly for DOM access.

That is only a small part.

The deeper purpose is performance optimization.

Because unnecessary rendering is expensive.

Especially in:

  • Large dashboards
  • Complex forms
  • Realtime applications
  • Charts
  • Data-heavy UIs

Refs help avoid render cycles for data that users never even see.

This keeps React applications faster and cleaner.


When You SHOULD Use useRef

Use refs when:

✅ Value should persist
✅ UI does NOT depend on the value
✅ Re-rendering is unnecessary
✅ You need DOM access
✅ You manage timers or intervals
✅ You store previous values
✅ You integrate third-party libraries


When You Should NOT Use useRef

Avoid refs when:

❌ UI depends on the value
❌ User should see updates instantly
❌ Data drives rendering
❌ Reactive updates are needed

In those cases:

Use state.

Because state exists specifically for rendering logic.


The Biggest Secret About useRef

Here is the hidden truth many developers miss:

useRef is one of the few hooks that gives you mutable memory inside React’s functional world.

Normally functional components are recreated every render.

But refs break that limitation.

They allow values to survive independently from rendering.

That makes refs feel almost like instance variables from class components.

And this is exactly why advanced React developers rely on refs heavily in performance-sensitive systems.


Final Thoughts

Understanding useRef changes the way you think about React.

Because now you realize:

Not every changing value belongs in state.

Some values are meant only for memory.

Some values are meant only for logic.

Some values should exist silently in the background without forcing React to repaint the UI repeatedly.

And that is precisely the role of useRef.

It gives React components memory without rendering pressure.

That balance between persistence and performance is what makes refs incredibly powerful.


Conclusion

That’s it for Episode 11 of the series: “Let’s Master React Hooks Together”

At first glance, useRef may appear tiny compared to hooks like useState or useEffect.

But after understanding it deeply, something becomes clear:

useRef is not just another React hook.

It is one of the core building blocks behind performant React applications.

Because real-world React development is not only about rendering UI.

It is also about controlling memory, preserving values, avoiding unnecessary renders, and managing behavior efficiently behind the scenes.

And that is exactly what refs are designed for.

You now deeply understand:

  • What useRef really is
  • Why React created it
  • The rendering problem it solves
  • How refs work internally
  • Why refs don’t trigger re-renders
  • How React preserves refs
  • DOM access patterns
  • Performance optimization strategies
  • Advanced real-world use cases
  • Stale closure solutions
  • Previous value tracking
  • Common mistakes developers make
  • The hidden philosophy behind refs

More importantly, you now understand a deeper React principle that changes how applications are designed:

React rendering should happen only when the UI actually needs updating.

That single idea is the foundation of scalable React architecture.

And useRef plays a massive role in achieving that balance.

The moment developers stop putting every changing value into state is usually the moment they start writing cleaner, smarter, and more optimized React applications.

Because mastering React is not only about making things work.

It’s about understanding:

  • what should trigger rendering,
  • what should persist silently,
  • and how React manages both worlds internally.

And useRef sits exactly at the center of that understanding.

See you in the next episode of “Let’s Master React Hooks Together”, where we continue exploring the deeper mechanics behind React Hooks and uncover how React

Top comments (0)