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:
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:
- The button text to change based on playing state (requires re-render)
- The
utterance.onendcallback 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
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.
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:
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:
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 allWhy duplication is bad
- Two sources of truth - Easy to update one and forget the other
- Effect chains - Using effects to sync state and ref creates unnecessary side effects
- 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:
- Can I update state directly in the callback instead of reading it?
- Can I restructure to avoid the async issue?
- 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