useRef returns a mutable container whose .current property persists across renders without triggering a re-render when mutated. That makes it the right tool for storing DOM nodes, timer ids, the latest value of some state, instance-like variables on a function component, and WebSocket or observer handles. Because writing to .current does not schedule a render, refs are also the standard escape hatch when you need to read or mutate something synchronously inside a callback. Two TypeScript flavors matter: RefObject<T> exposes { current: T \| null } and is set by React, while MutableRefObject<T> exposes { current: T } and is something you manage directly.
The canonical pattern for reading the latest state inside a long-lived callback is the ref-plus-effect combo. You create a ref, then in an effect you assign the current state into the ref on every render. Inside the callback — say, an interval or an event listener — you read ref.current instead of the closed-over state variable. This is how you fix the classic stale-closure trap where setInterval(() => console.log(count), 1000) logs the initial value forever because the callback captured the count from the first render. A usePrev hook (returning the value from the previous render) and a useIsMounted hook (returning whether the component is still mounted) follow the same pattern: assign in an effect, read in callbacks. Stable callbacks that always see the latest state have been an open problem; the community pattern combines useRef with useCallback to produce a function whose identity never changes but whose body always reads the freshest state, and the official useEvent / useEffectEvent RFC (available in React canary) formalizes this with a stable callback that closes over the latest values without forcing effect deps to change.
Refs also escape the declarative model when you genuinely need imperative handles. forwardRef passes a ref through a component to a child DOM node, and useImperativeHandle customizes what the parent sees — for example, exposing only { focus, clear } instead of the raw DOM element. React 19 simplifies this significantly: function components now accept ref as a regular prop, removing the need for forwardRef in most cases. Ref callbacks themselves gained a superpower: in React 19, a ref callback can return a cleanup function that runs when the ref is detached, which makes merge-refs utilities and one-off subscriptions cleaner. A simple merge helper combines multiple refs into one callback that fans out to each. Use these imperative tools sparingly — exposing internal state up the tree is usually a sign you should lift that state to a parent instead.