React useRef Hook

In the last lesson on React When Not to Use useEffect you learned when an effect is the wrong tool. This lesson is about useRef, the hook for when you want a component to remember a value between renders without repainting the screen.

🤔 Why do we need useRef?

State has one rule that gets in the way sometimes. Changing state always re-renders the component, but now and then you want memory without that repaint:

  • You might want to count how many times a component rendered, just to log it. You don’t want that count to trigger more renders.
  • You might want to remember the previous value of something, only to compare it.
  • You might want to hold a timer id so you can stop the timer later.
  • And later on, you’ll want to point straight at a real element on the page, like an input box.

None of that should repaint the screen. So you need a box that holds a value, keeps it across renders, but stays quiet when it changes. That box is useRef.

🧩 What is useRef?

useRef is a React hook that gives you a small box with one slot inside it, called .current. That box is a ref object, and the value in .current stays the same across renders.

  • You call useRef and it hands you back one object.
  • That object always has a property called .current. That’s where your value lives.
  • The box survives every render. React does not throw it away and start over.
  • You can read .current and you can write to .current freely.

Think of it like a sticky note taped to the component. The note stays stuck there render after render. You can rub out what’s written and write something new any time, and nobody gets alerted when you do.

🛠️ The syntax

Here’s the shape of it. You call useRef with a starting value, and you get a ref object back.

This line creates a ref whose .current starts at 0, then reads and writes that slot.

import { useRef } from "react";
const myRef = useRef(0); // myRef.current is 0 to start
myRef.current; // read the value
myRef.current = 5; // write a new value

Let’s read the parts.

  • useRef(0) creates the box and puts 0 into .current as the initial value.
  • myRef.current is how you read what’s inside, any time you want.
  • myRef.current = 5 is how you change it. Plain assignment is fine here.
  • Notice you never call a setter. With state you’d call setCount. With a ref you just write to .current directly.

⚡ Changing a ref does not re-render

This is the one idea to really hold onto. It’s the whole reason useRef exists.

  • When you write myRef.current = something, the value changes right away in memory.
  • But React does not re-render the component. The screen stays exactly as it was.
  • So a ref is perfect for a value you want to remember but do not want to show.
  • If you change .current and then expect the screen to update on its own, it will not. That’s the trap to watch for.

A ref change is silent

Writing to myRef.current updates the value but never tells React to repaint. If a value needs to appear on screen and update when it changes, that value belongs in state, not a ref.

💡 A simple example: count renders without re-rendering

Let’s see a value that survives renders but stays quiet. We’ll count how many times the component rendered, using a ref so counting up does not cause even more renders.

This component keeps a render counter in a ref, bumps it on every render, and logs the number.

import { useState, useRef } from "react";
function RenderCounter() {
const [text, setText] = useState("");
const renderCount = useRef(1);
console.log(`Rendered ${renderCount.current} times`);
renderCount.current = renderCount.current + 1;
return (
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Type something"
/>
);
}

Let’s walk through what happens here.

  • useRef(1) makes a box with 1 inside. This is our render count.
  • Every time the component renders, console.log prints the current count.
  • Then renderCount.current = renderCount.current + 1 bumps it up for next time.
  • Typing in the input changes text, which is state, so React re-renders. Each render the log goes up: 1, then 2, then 3.
  • Here’s the key part. Bumping renderCount.current does not cause its own render. Only the state change does that, so the ref quietly keeps count in the background.

Console output after typing three letters

Rendered 1 times
Rendered 2 times
Rendered 3 times

Why a plain variable won't do this

You might think “I’ll just use let count = 1.” But a plain variable resets to 1 on every render, so it can never count up. A ref holds its value across renders, which is exactly what makes this work.

🆚 ref vs state

People mix these two up, so let’s put them side by side. Both remember a value across renders. The big difference is the re-render: changing state repaints the screen, changing a ref does not.

Question State (useState) Ref (useRef)
Does changing it re-render? Yes, that’s the whole point No, it changes silently
How do you change it? Call the setter, like setCount(5) Write to it directly, myRef.current = 5
Does it survive across renders? Yes Yes
Best for… Data the user should see on screen Data to remember but not show

A quick rule of thumb: if the value belongs on the screen, use state. If you just need to remember it quietly, use a ref.

🎯 The two big jobs for useRef

So why does this hook matter? It does two main jobs, and each one gets its own lesson coming up.

  • Hold a mutable value that survives renders. That’s the render counter you just saw. A previous value, a timer id, any value you want to keep but not display.
  • Point to a DOM element. You can attach a ref to a real element on the page, like an input, and then reach it directly. For example, to focus that input when the page loads.

Both jobs use the same .current slot. The only difference is what you store in it: a value of your own, or a real element handed to you by React.

One hook, two uses

Keep both in mind: a ref can hold any mutable value you want to remember quietly, and a ref can also point to a DOM element so you can work with it directly. Same hook, same .current, two everyday uses.

🧩 What You’ve Learned

  • useRef creates a ref object with a .current property that persists across renders
  • ✅ The syntax is const myRef = useRef(initialValue), and you read or write myRef.current
  • ✅ You change a ref by writing to .current directly, no setter needed
  • ✅ Changing .current does not cause a re-render, unlike changing state
  • ✅ A ref is perfect for a value you want to remember but not show, like a render count or a previous value
  • ✅ ref vs state: both survive renders, but only state repaints the screen when it changes
  • ✅ The two main jobs are holding a mutable value and pointing to a DOM element, each covered next

Check Your Knowledge

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    What does useRef give you back?

    Why: useRef returns a ref object that always has a .current property. That slot holds your value and keeps it across renders.

  2. 2

    What happens when you change myRef.current?

    Why: Writing to .current updates the value silently. It does not trigger a re-render, which is the main difference from state.

  3. 3

    How do you change the value held in a ref?

    Why: A ref has no setter. You read and write its .current property directly, for example myRef.current = 5.

  4. 4

    When should you use a ref instead of state?

    Why: Use a ref for a value you want to remember quietly, with no repaint. If the value should show on screen and update, use state instead.

🚀 What’s Next?

You’ve got the overview now: a ref is a quiet box with a .current slot that survives renders and does not repaint when it changes. Next you’ll dig into the first big job, pointing a ref straight at a real element on the page so you can work with it directly.

React Accessing DOM Elements

Share & Connect