Skip to content

Do You Need Both State and Ref for the Same Value in React?

Problem

When I was building an audio player component with the Speech Synthesis API, I found myself doing this:

AudioPlayer.jsx
function AudioPlayer() {
const [isPlaying, setIsPlaying] = useState(false);
const isPlayingRef = useRef(false);
useEffect(() => {
isPlayingRef.current = isPlaying;
}, [isPlaying]);
const handlePlay = () => {
const utterance = new SpeechSynthesisUtterance('Hello');
speechSynthesis.speak(utterance);
setIsPlaying(true);
utterance.onend = () => {
if (isPlayingRef.current) {
setIsPlaying(false);
}
};
};
return (
<button onClick={handlePlay}>
{isPlaying ? 'Playing...' : 'Play'}
</button>
);
}

I had the same isPlaying value stored in both useState and useRef. Something felt wrong about this, but I wasn’t sure what.

Environment

  • React 18.2
  • Browser Speech Synthesis API

What happened?

I was trying to track whether audio was playing. I needed:

  1. The button text to change based on playing state (requires re-render)
  2. The utterance.onend callback to access the current playing state

I thought I needed useState for the UI and useRef for the callback. So I duplicated the value and tried to keep them in sync with a useEffect.

But this creates problems:

  • Two sources of truth that can get out of sync
  • Extra re-renders from the sync effect
  • More code to maintain
  • A code smell that something is wrong with my approach

How to solve it?

I tried to simplify. First, I asked myself: do I really need both?

Attempt 1: Remove the ref, use only state

AudioPlayer-StateOnly.jsx
function AudioPlayer() {
const [isPlaying, setIsPlaying] = useState(false);
const handlePlay = () => {
const utterance = new SpeechSynthesisUtterance('Hello');
utterance.onend = () => {
setIsPlaying(false);
};
speechSynthesis.speak(utterance);
setIsPlaying(true);
};
return (
<button onClick={handlePlay}>
{isPlaying ? 'Playing...' : 'Play'}
</button>
);
}

This works. The setIsPlaying(false) inside the callback updates state directly. React handles the rest.

Wait, but what about the stale closure problem I was worried about?

The truth is, in this case, there’s no stale closure issue. The onend callback doesn’t need to read the current state - it just needs to set it to false. I was overcomplicating things.

Attempt 2: When would I actually need a ref?

Let me think of a case where the callback needs to READ the latest value, not just write it.

DelayedSend.jsx
function DelayedSend() {
const [text, setText] = useState('');
const handleSend = () => {
setTimeout(() => {
// Problem: This captures 'text' at click time
console.log('Sending:', text);
}, 3000);
};
return (
<>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button onClick={handleSend}>Send (delayed 3s)</button>
</>
);
}

Here, if I type “Hello”, click send, then type “World”, the console logs “Hello” not “World”. That’s the stale closure problem.

Now a ref makes sense:

DelayedSend-WithRef.jsx
function DelayedSend() {
const [text, setText] = useState('');
const textRef = useRef(text);
function handleChange(e) {
setText(e.target.value);
textRef.current = e.target.value;
}
const handleSend = () => {
setTimeout(() => {
console.log('Sending:', textRef.current);
}, 3000);
};
return (
<>
<input value={text} onChange={handleChange} />
<button onClick={handleSend}>Send (delayed 3s)</button>
</>
);
}

This is one of the rare cases where having both state and ref for the same value is acceptable.

The reason

The key insight is understanding what each hook is for:

useState - Use when the value affects what’s rendered. Changes trigger re-renders.

useRef - Use when you need a mutable value that persists across renders but shouldn’t trigger re-renders. Think of it as a “box” where you can put anything.

Here’s a simple decision flow:

Decision flowchart
Does the value affect what's rendered?
|
+-- YES --> Use useState
|
+-- NO --> Do you need the value in callbacks/effects?
|
+-- YES --> Use useRef
|
+-- NO --> You might not need it at all

Why duplication is bad

  1. Two sources of truth - Easy to update one and forget the other
  2. Effect chains - Using effects to sync state and ref creates unnecessary side effects
  3. Fighting React - Usually signals you’re working against React’s model instead of with it

When both might be needed

The React docs acknowledge one valid pattern: when you need state for rendering AND need to read the latest value in an async callback.

But before reaching for this pattern, ask:

  1. Can I update state directly in the callback instead of reading it?
  2. Can I restructure to avoid the async issue?
  3. Is there a simpler solution?

In my audio player example, I didn’t need to read the state - I just needed to set it. So state alone was enough.

Summary

In this post, I explored why duplicating a value in both useState and useRef is usually an anti-pattern. The key point is to choose state when the value affects rendering, and ref when you need a mutable value without re-renders. Most of the time, you only need one of them.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments