跪拜 Guibai
← All articles
Frontend · React.js · AI Programming

How useRef Became the Escape Hatch That Fixes React's Closure Traps and Rerenders

By 寅时码 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

React's dependency arrays and closure traps are the primary source of performance degradation and debugging pain in large codebases. A `useRef`-based pattern that requires zero migration cost — no compiler, no Signals, no framework switch — offers a practical off-ramp for teams stuck with vanilla React who still need stable callbacks and controllable rendering.

Summary

React's `useCallback` forces developers to list every value they read as a dependency, turning a simple read into a subscription. When those dependencies change, the callback reference changes, piercing `memo` and triggering recursive rerenders down the entire subtree. The official React Compiler, now being rewritten in Rust largely by AI, promises automatic memoization but introduces its own black-box opacity and incompatibilities with tools like Preact Signals.

The fix centers on `useLatestCallback`, a hook that stores the latest function in a `useRef` updated during the commit phase via `useInsertionEffect`. It returns a stable callback shell that always calls the most recently committed logic, giving developers both reference stability (preserving `memo`) and up-to-date closures without a dependency array. The same `useRef`-based approach extends to a Suspense-powered KeepAlive that preserves Effects, a Vue-style router with built-in LRU page caching and global guards, and convenience wrappers for async Effects.

The entire set of conventions has been codified into a Claude Code skill, so AI assistants default to writing React that avoids the framework's most persistent footguns.

Takeaways
`useCallback` forces a subscription model: every value read must be listed as a dependency, and when any dependency changes, the callback reference changes, piercing `memo` and triggering subtree rerenders.
React's official stance — "avoid memo unless you have a performance problem" — shifts the burden onto developers, who then face near-impossible debugging when rerenders cascade at scale.
The React Compiler, now stable, is being rewritten in Rust with the initial version largely coded by Claude; it remains a black box that breaks with tools like Preact Signals and weakens developer control over rendering.
`useRef` is the only React primitive that can hold a mutable value without triggering a render, making it the sole escape hatch for hacking React's rendering behavior.
`useLatestCallback` stores the latest function in a ref updated during the commit phase (`useInsertionEffect`), returning a stable callback that always calls the most recently committed logic — no dependency array needed.
ahooks' `useMemoizedFn` writes to the ref during the render phase, risking pollution from aborted concurrent renders; `useLatestCallback` waits until commit, keeping the ref aligned with the on-screen UI.
React's `useEffectEvent` was deliberately narrowed to Effect-only logic and cannot be passed to child components or used as a general event handler, abandoning the original `useEvent` RFC's promise.
A Suspense + `throw Promise` KeepAlive preserves regular `useEffect`s when hidden, unlike the official `<Activity>` which destroys all Effects on hide with no opt-out.
Mainstream React routers (react-router v8, TanStack Router) still do not ship built-in page-component KeepAlive as of mid-2026.
StrictMode and the "no conditional Hooks" rule both stem from React's fragile core model — render must be pure and Hooks rely on call order — a fragility other frameworks avoid by not re-running component functions on every update.
Conclusions

React's dependency array problem is not a minor inconvenience but a structural defect: the framework conflates "reading a value" with "subscribing to changes," and the official remedy (React Compiler) swaps one black box for another.

The `useRef`-as-escape-hatch pattern succeeds precisely because it exploits React's indifference — `useRef` is too simple for React to manage, so it becomes the only place developers can safely stash mutable values outside the render cycle.

React's design philosophy has inverted: a framework that once rejected magic now ships a compiler whose Rust rewrite was largely AI-generated, while the community independently rebuilds the abandoned `useEvent` RFC in userland.

Other frameworks (Vue, Solid, Svelte) avoid the "no conditional Hooks" rule entirely because they don't re-execute component functions on every update — React's core model is the root cause, not a necessary tradeoff.

The Suspense-based KeepAlive and the official `<Activity>` represent two fundamentally different philosophies: preserve side effects during hide versus treat hide as conceptual unmount. Activity's lack of configurability forces a choice the developer should be making.

Concepts & terms
useLatestCallback
A userland hook that stores the latest function in a `useRef` updated during the commit phase via `useInsertionEffect`, returning a `useCallback`-wrapped stable shell. It provides both reference stability (preserving downstream `memo`) and access to the most recently committed closure, without requiring a dependency array.
useInsertionEffect
A React hook that runs synchronously during the commit phase, before layout and passive effects. Officially intended for CSS-in-JS style insertion, it is repurposed here to update refs with the latest committed value, avoiding pollution from aborted concurrent renders.
Suspense + throw Promise KeepAlive
A KeepAlive implementation that throws a Promise during render when inactive, caught by a parent `<Suspense>` to hide the subtree. Unlike the official `<Activity>`, it preserves regular `useEffect` subscriptions and timers while hidden, only cleaning up layout effects.
StrictMode double-invocation
React's development-only checker that calls render functions and Effect setups twice to detect impure rendering and missing cleanup. It exists because React's core model (render must be pure, Effects must be symmetric) has no language-level guarantees.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗