Skip to main content

useReference

Creates a ref that always contains the current value, updating automatically on every render. This hook solves the "stale closure" problem by ensuring that async callbacks, effects, and event handlers always have access to the latest value without needing to recreate themselves. Unlike a regular useRef, this ref's current value is updated on every render to match the provided value.

Problem it Solves

When callbacks are created in React, they capture values from their closure. If these values change later, the callback still sees the old values. This hook ensures you always have access to the current value without recreating callbacks.

Use Cases

  • Async Callbacks: Access current state in setTimeout/setInterval callbacks
  • Event Handlers: Use latest props/state in debounced or throttled handlers
  • Effect Cleanup: Access current values in cleanup functions
  • Imperative Handles: Expose methods that use current component state
  • External Library Callbacks: Provide callbacks to non-React code

Comparison with Alternatives

  • vs useRef: This updates automatically; useRef requires manual updates
  • vs useState: This doesn't trigger re-renders when updated
  • vs useCallback deps: This avoids recreating callbacks when values change

Signature

const useReference: <T>(value: T) => import("react").RefObject<T>

Parameters

NameTypeDescription
value-The value to track. The ref will always contain this latest value

Returns

A ref object whose current property always equals the latest value

Examples

Example 1

// Problem: Stale state in async callback
const [count, setCount] = useState(0);
const handleAsync = useCallback(async () => {
await delay(1000);
console.log(count); // Always logs 0, even if count changed
}, []); // Can't add count to deps or callback recreates

// Solution: Using useReference
const [count, setCount] = useState(0);
const countRef = useReference(count);
const handleAsync = useCallback(async () => {
await delay(1000);
console.log(countRef.current); // Always logs current count
}, [countRef]); // countRef never changes, so callback is stable

// Access current props in cleanup
const connectionRef = useReference(props.connection);
useEffect(() => {
const ws = new WebSocket(url);

return () => {
// Access current connection state during cleanup
if (connectionRef.current.shouldNotify) {
notifyDisconnection();
}
ws.close();
};
}, [url, connectionRef]);

// Debounced handler with current values
const searchTerm = useReference(inputValue);
const debouncedSearch = useMemo(
() => debounce(() => {
// Always searches with current term
search(searchTerm.current);
}, 500),
[searchTerm]
);

// Imperative handle with current state
const [items, setItems] = useState([]);
const itemsRef = useReference(items);

useImperativeHandle(ref, () => ({
getItemCount: () => itemsRef.current.length,
hasItems: () => itemsRef.current.length > 0,
}), [itemsRef]);

Playground

// Problem: Stale state in async callback
const [count, setCount] = useState(0);
const handleAsync = useCallback(async () => {
await delay(1000);
console.log(count); // Always logs 0, even if count changed
}, []); // Can't add count to deps or callback recreates

// Solution: Using useReference
const [count, setCount] = useState(0);
const countRef = useReference(count);
const handleAsync = useCallback(async () => {
await delay(1000);
console.log(countRef.current); // Always logs current count
}, [countRef]); // countRef never changes, so callback is stable

// Access current props in cleanup
const connectionRef = useReference(props.connection);
useEffect(() => {
const ws = new WebSocket(url);

return () => {
  // Access current connection state during cleanup
  if (connectionRef.current.shouldNotify) {
    notifyDisconnection();
  }
  ws.close();
};
}, [url, connectionRef]);

// Debounced handler with current values
const searchTerm = useReference(inputValue);
const debouncedSearch = useMemo(
() => debounce(() => {
  // Always searches with current term
  search(searchTerm.current);
}, 500),
[searchTerm]
);

// Imperative handle with current state
const [items, setItems] = useState([]);
const itemsRef = useReference(items);

useImperativeHandle(ref, () => ({
getItemCount: () => itemsRef.current.length,
hasItems: () => itemsRef.current.length > 0,
}), [itemsRef]);