eslint-plugin-react-hooks 7 Guide: Fix set-state-in-effect and refs Warnings in React
eslint-plugin-react-hooks 7 is surfacing stricter React warnings around effects and refs. Here is what changed, why it matters, and how to fix the noisy patterns fast.
If your React app suddenly started throwing new lint warnings after a dependency update, no, you did not wake up in a cursed codebase. The bigger change is that eslint-plugin-react-hooks 7 is a lot less polite than older versions, especially around effects, refs, and patterns that quietly add extra renders.
That is actually good news. Most of these warnings are not nitpicks. They are the kind of small architectural smells that make React apps feel heavier than they should, and they line up with how the React team keeps pushing codebases toward compiler-friendly, render-pure patterns.
So if you have been staring at messages like react-hooks/set-state-in-effect or new refs warnings and wondering what changed, here is the practical version without turning this into a lint rule archaeology lesson.
What Changed in eslint-plugin-react-hooks 7
The April 2026 update is not just a version bump for people who like clean lockfiles. The plugin now supports ESLint 10, skips compilation work for non-React files, and improves a few compiler-related diagnostics that real apps actually trip over.
Better detection for synchronous state updates inside effects
Stronger refs validation during render
Cleaner error reporting for compiler diagnostics
A smaller performance hit in mixed codebases because non-React files are skipped
A quick 7.1.1 follow-up that restores compatibility for configs referencing component-hook-factories
The important part is not the changelog wording. The important part is what this feels like in daily work: rules that used to miss questionable patterns now catch them more reliably, which means code that used to slip through lint can suddenly light up after an upgrade.
The Warning Most Teams Will Hit First: set-state-in-effect
This rule targets a very common React habit: deriving one piece of state from another piece of state or from props inside useEffect. It often works, but it usually costs you an avoidable render and makes the component harder to reason about.
The old pattern looks harmless
function UserList({ users }) { const [sortedUsers, setSortedUsers] = useState([]); useEffect(() => { setSortedUsers([...users].sort(sortByName)); }, [users]); return <List items={sortedUsers} />;}Nothing here looks dramatic, but React has to render once, run the effect, update state, and then render again. That second pass exists only because we moved a pure calculation into an effect.
The better move is usually render-time derivation
function UserList({ users }) { const sortedUsers = useMemo( () => [...users].sort(sortByName), [users] ); return <List items={sortedUsers} />;}And if the calculation is cheap, you often do not even need useMemo. Just derive the value during render and move on. The rule is really nudging you toward one question: is this synchronizing with an external system, or am I just rearranging data I already have?
That same advice applies to the classic loading-state pattern too. If your effect starts by doing setLoading(true) every mount, you are often better off initializing loading state up front or using request state from the data layer instead of paying for a sync effect update.
Why the Refs Rule Is Suddenly Showing Up
The other warning people are bumping into is the refs rule. In plain English, React does not want you reading mutable ref values during render as if they were stable inputs. Render is supposed to be pure. Refs are escape hatches.
function Dialog({ dialogRef }) { const width = dialogRef.current?.offsetWidth ?? 0; return <div style={{ width }} />;}That kind of code feels convenient, but it mixes layout measurement into render. If the width matters because you are reading from the DOM, do it in a layout effect or an event handler where the timing actually matches what you are trying to observe.
function Dialog({ dialogRef }) { const [width, setWidth] = useState(0); useLayoutEffect(() => { setWidth(dialogRef.current?.offsetWidth ?? 0); }, []); return <div style={{ width }} />;}This is a nice contrast with set-state-in-effect: setting state in an effect is not always wrong. It is wrong when you are doing work that could have happened during render. Reading layout from a ref is different because that value only exists after React has committed to the DOM.
This Is Really About React Compiler, Even If You Are Not Using It Yet
One reason these rules are getting sharper is that the plugin now doubles as a delivery mechanism for React Compiler diagnostics. That matters even if your app is not fully compiler-enabled today.
The React docs make this pretty explicit: compiler diagnostics surfaced through the plugin can help you gradually fix code that the compiler would skip. So the lint update is not just about cleaner code style. It is about removing patterns that block future optimizations.
Fewer extra renders from derived state living in effects
Cleaner dependency tracking around hooks and effect events
Less render-time impurity from refs and mutable values
A smoother path if your team wants compiler-friendly components later
If you want the exact rule behavior, the React docs page for set-state-in-effect is worth bookmarking here. It explains the distinction between invalid derived state patterns and valid ref-based measurement cases better than most code review comments ever will.
A Practical Upgrade Checklist
If your codebase just adopted 7.1.x and the warning count jumped, resist the urge to mass-disable everything. The faster move is to fix the patterns with the highest leverage first.
Upgrade straight to 7.1.1 if you hit config noise around component-hook-factories
Fix derived state in effects before touching smaller style-only warnings
Treat new refs warnings as render-purity issues, not as random lint false positives
Roll the noisier rules out as warnings first if the codebase is large
Audit old useEffect blocks that only transform props, arrays, or IDs into other values
Re-check any custom lint config if you manage rules manually instead of using the recommended preset
A lot of teams will discover that the real cleanup is not in the ESLint config. It is in simplifying components that slowly turned effects into a catch-all place for data shaping, state resets, and DOM reads.
TL;DR
eslint-plugin-react-hooks 7 is stricter in useful ways, especially around effects and refs
The biggest new pain point for most teams will be better set-state-in-effect detection
If a value can be derived during render, do that instead of bouncing it through state
Reading refs during render is usually a purity smell, not a harmless shortcut
This lint upgrade is really a preview of where React wants app architecture to go next