Why Does ESLint Warn When You Initialize useRef with State?
Problem
When I tried to create a ref that tracks a state value, I got this ESLint error:
ESLint: Modifying a value previously passed as an argument to a hook is not allowed.Consider moving the modification before calling the hook.Here’s the code that triggered it:
import { useState, useRef } from 'react';
function MediaPlayer() { const [isPlaying, setIsPlaying] = useState(false);
// ESLint error here! const isPlayingRef = useRef(isPlaying);
const handlePlay = () => { setIsPlaying(true); };
return <button onClick={handlePlay}>Play</button>;}I was confused. I thought refs were meant to be mutable. Why does ESLint complain?
Environment
- React 18.x
- ESLint with react-hooks plugin
What happened?
I wanted to keep a ref in sync with my isPlaying state. My reasoning was simple: pass the state value to useRef(), and the ref gets initialized with that value.
But ESLint flagged this line:
const isPlayingRef = useRef(isPlaying);The warning says I’m “modifying a value previously passed as an argument to a hook.” But I’m not modifying anything! I’m just reading isPlaying.
Here’s what I didn’t understand at the time.
The reason
The issue is about how React hooks work, not about refs being mutable.
When you call useRef(initialValue), React only uses that initialValue on the first render. On subsequent renders, React ignores the argument entirely and returns the existing ref object.
But ESLint’s rule doesn’t know this. It sees:
isPlayingis a state variable (can change)- You’re passing it to a hook as an argument
- The hook’s argument should be “stable”
ESLint treats hook arguments as something that shouldn’t change between renders. State values, by definition, can change. So passing state to useRef() triggers the warning.
Here’s the confusing part: even though refs are designed to be mutable (you can freely assign to ref.current), the initialization argument is treated as immutable by ESLint.
How to solve it?
I tried a few approaches.
First attempt: Disable the ESLint rule
// DON'T DO THISconst isPlayingRef = useRef(isPlaying); // eslint-disable-lineThis “works” but it’s wrong. The warning exists for a reason. I was just hiding the problem.
Second attempt: Initialize with null
I read the React docs more carefully. The recommended pattern is to initialize refs with null or a static value:
import { useState, useRef, useEffect } from 'react';
function MediaPlayer() { const [isPlaying, setIsPlaying] = useState(false);
// Initialize with null - ESLint is happy const isPlayingRef = useRef(null);
// Sync ref with state explicitly useEffect(() => { isPlayingRef.current = isPlaying; }, [isPlaying]);
const handlePlay = () => { setIsPlaying(true); // ref.current will be updated via effect };
return <button onClick={handlePlay}>Play</button>;}This works. The ESLint warning is gone.
The pattern is:
- Initialize the ref with
null(a stable value) - Sync the ref with state in a
useEffect - The effect runs on mount and whenever
isPlayingchanges
The behavior is identical to what I originally wanted, but now the code follows React conventions.
For expensive initializations
Sometimes you need to initialize a ref with a computed value, not null. React docs show this pattern:
import { useRef } from 'react';
function VideoPlayer() { const playerRef = useRef(null);
// Conditional assignment in render phase if (playerRef.current === null) { playerRef.current = new VideoPlayer(); }
return <div>Video Player</div>;}This is safe because:
- The
useRef(null)call has a stable argument - The conditional logic happens after the hook call
- It only runs once (on first render when
currentisnull)
A common mistake
I initially thought that useRef(isPlaying) would keep the ref updated when state changes. It doesn’t.
// This does NOT keep the ref in sync!const isPlayingRef = useRef(isPlaying);
// isPlayingRef.current stays at the initial value forever// even when isPlaying state changesThe initial value is only used once, on the first render. If you want the ref to track state changes, you need the useEffect pattern I showed above.
Summary
In this post, I explained why ESLint warns when you initialize useRef with a state value. The key point is that hook arguments should be stable, even though refs themselves are mutable.
The fix is simple: initialize with useRef(null) instead of useRef(stateValue), and sync the ref in a useEffect if needed.
This pattern:
- Avoids ESLint warnings
- Follows React conventions
- Makes the data flow explicit
- Works exactly the same as the “wrong” version
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