useLiveRef is a hook that always returns the same ref object whose .current property is updated on every render to the latest provided value. It’s useful when you need to read fresh values inside callbacks, effects, timers, event handlers, etc., without resubscribing or triggering re-renders.
function useLiveRef<Value>(value: Value): React.RefObject<Value>;- Parameters
value: Value— the current value that should be available viaref.current.
- Returns
RefObject<Value>— a stablerefobject (does not change between renders) whoseref.currentalways points to the latestvalue.
import { useEffect } from 'react';
import { useLiveRef } from '@webeach/react-hooks/useLiveRef';
export function CursorTracker({ enabled }: { enabled: boolean }) {
const enabledRef = useLiveRef(enabled);
useEffect(() => {
const handleMove = (event: MouseEvent) => {
// Read the up-to-date flag from the ref instead of a stale first-render closure
if (!enabledRef.current) {
return;
}
// ...handler logic
};
window.addEventListener('mousemove', handleMove);
return () => window.removeEventListener('mousemove', handleMove);
}, [enabledRef]); // enabledRef is stable; the effect isn’t recreated when `enabled` changes (you can omit it from deps if you prefer)
return null;
}import { useEffect } from 'react';
import { useLiveRef } from '@webeach/react-hooks/useLiveRef';
export function Poller({ intervalMs, onTick }: { intervalMs: number; onTick: () => void }) {
const onTickRef = useLiveRef(onTick);
useEffect(() => {
const id = setInterval(() => {
// No need to recreate the interval when `onTick` changes
onTickRef.current();
}, intervalMs);
return () => clearInterval(id);
}, [intervalMs, onTickRef]);
return null;
}import { useCallback, useState } from 'react';
import { useLiveRef } from '@webeach/react-hooks/useLiveRef';
export function Example() {
const [count, setCount] = useState(0);
const countRef = useLiveRef(count);
// memoized callback doesn’t depend on `count`, but reads the fresh value inside
const log = useCallback(() => {
console.log('current count =', countRef.current);
}, []);
const handleButtonClick = () => {
setCount((c) => c + 1);
log();
};
return (
<button onClick={handleButtonClick}>
increment & log
</button>
);
}-
Stable
refobject- The returned
refis created once and does not change between renders. This lets you safely include it in effect/callback dependency arrays without unnecessary re-creations.
- The returned
-
.currentupdates on every render- Inside the hook,
ref.current = valueruns on each render, so handlers always see the freshest value.
- Inside the hook,
-
No extra re-renders
- Updating
ref.currentdoes not cause a component re-render.
- Updating
-
SSR/ISR
- The hook doesn’t touch
window/documentand is safe to run on the server. During SSR,ref.currentequals the lastvalueprovided at render time.
- The hook doesn’t touch
-
Compared to alternatives
- Unlike plain
useRef(initial), where you must update.currentyourself,useLiveRefdoes it automatically on each render. - Unlike storing the value in
useState,useLiveRefdoesn’t trigger re-renders when.currentchanges. - Unlike a closure,
useLiveRefdoesn’t “freeze” values: handlers read the current one.
- Unlike plain
- You need the latest value inside long-lived subscriptions:
addEventListener,MutationObserver,ResizeObserver,setInterval,requestAnimationFrame, WebSocket callbacks, etc. - You want to keep callbacks and effects stable (not depending on frequently changing values) while still reading fresh data.
- If you need the UI to react to value changes — use
useState/useReducer.useLiveRefdoes not re-render the component.
-
Expecting re-renders from a
ref- Updating
ref.currentdoes not trigger a re-render. If your UI depends on that value, use state.
- Updating
-
Putting volatile values into effect deps
- The goal of
useLiveRefis to avoid unnecessary effect/callback re-creations. Keep the ref itself in deps rather than the changing value living inside it.
- The goal of