ref-patterns — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited ref-patterns (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Ref Patterns is the discipline of choosing, forwarding, and exposing React refs deliberately, with a single sharp rule at its center: refs are the intentional hole in React's "UI is a pure function of state and props" contract. A ref is a mutable container — { current: T } — that React creates once per component instance and preserves across renders; writing to ref.current does not trigger a re-render and reading from it does not subscribe the component to changes, so the ref lives entirely outside the reactive graph. That escape-hatch nature makes refs right for exactly two families of values — a stable handle to a DOM node (for focus, measurement, animation, or handoff to a non-React library) and a mutable instance value the component needs but should not render (an interval id, a requestAnimationFrame id, a latest-arguments closure, a previous-value snapshot) — and wrong everywhere else, because a ref used as a substitute for state silently breaks the contract: the value changes, the UI does not update, and the bug surfaces remote from its cause. On top of the useRef primitive sit four layered mechanics this skill owns end to end: ref callbacks for synchronous mount/unmount hooks and ref composition, forwardRef (React 18 and earlier) and the React 19 ref-as-prop change that retires it for new code, useImperativeHandle for exposing a minimal controlled imperative surface to a parent, and the requirement that every compound-component primitive forward refs through to its underlying DOM element. It deliberately does not own the broader hook discipline (hooks-patterns), state location and ownership (state-management), the client/server serialization boundary (client-server-boundary), cross-product component layering (component-architecture), or form-state design (form-ux-architecture) — it owns the not-state primitive and the sharp decision boundary between a ref and state.
The discipline of designing React ref usage: the conceptual distinction between refs (mutable handles that survive renders without triggering them) and state (reactive values that do trigger renders), the useRef hook for DOM access and mutable instance values, ref callbacks (ref={(node) => ...}) for fine-grained mount and unmount hooks, forwardRef for passing refs through component boundaries on React 18 and earlier, the React 19 ref-as-prop change that retires forwardRef for new code, useImperativeHandle for exposing a controlled imperative surface to a parent ref, the ref-forwarding pattern used inside compound-component primitives (Radix Slot / Headless UI), and the central design rule that refs are an escape hatch — appropriate for DOM access, focus management, animation, measurement, integration with non-React DOM libraries, and sparingly-exposed imperative APIs — never as a substitute for state.
React's rendering model is built on a single contract: the UI is a pure function of state and props. When state changes, the component re-renders; the new output is reconciled against the old; the DOM updates. This contract is what makes React components composable, testable, and reasonable.
Refs are the deliberate hole in that contract.
A ref is a mutable container — { current: T } — that React creates once per component instance and preserves across renders. Writes to ref.current do not trigger a re-render. Reads from ref.current do not subscribe to changes. The ref exists outside the reactive graph.
That's exactly what makes refs useful in the few places they belong:
requestAnimationFrame id, a previous-value snapshot, a "latest arguments" closure — these are values the component needs but should not render. Storing them in state would trigger pointless re-renders; storing them in refs keeps them out of the render path.open(), close(), focus(), scrollIntoView()) that doesn't naturally fit into the props-and-state model. A ref combined with useImperativeHandle gives the parent that surface in a controlled way.The same mutability that makes refs useful also makes them dangerous. A ref used as a substitute for state silently breaks React's contract — the value changes, the UI doesn't update, the bug surfaces somewhere remote from the cause. The design rule is sharp: if the value should cause a re-render when it changes, it is state; if it should not, it is a ref. No middle ground.
useRef — The Primitiveimport { useRef, useEffect } from 'react'
function AutoFocusInput() {
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
inputRef.current?.focus()
}, [])
return <input ref={inputRef} />
}useRef(initial) returns the same { current } object across every render of this component instance. The first render initializes current to the argument; subsequent renders return the same container with whatever current has become.
Two canonical shapes:
// DOM ref — initialized to null; React assigns the node on mount
const inputRef = useRef<HTMLInputElement>(null)
// Mutable instance value — initialized to a real value
const intervalIdRef = useRef<number | null>(null)
const latestPropsRef = useRef(props)In strict mode, refs survive React's intentional double-invocation of components and effects — they are tied to component instances, not to render passes.
| Symptom | Right primitive |
|---|---|
| The value drives what's rendered | useState |
| The value affects an effect's behavior but isn't shown | useRef (unless the effect needs to re-run when it changes, then useState) |
| You want a re-render when this changes | useState |
| You don't want a re-render when this changes | useRef |
| You're storing a DOM node | useRef (never useState — would trigger a re-render on every reconciliation pass) |
| You're storing a setTimeout / setInterval id | useRef |
| You're tracking "previous value" or "latest args" for an effect | useRef |
| You're caching an expensive value that depends on inputs | useMemo (not a ref) |
| You're holding form-input state | useState for controlled, useRef for genuinely uncontrolled |
The most common misuse: storing form values in refs to "avoid re-renders" — then needing to read those values during render and discovering the read is stale, because refs don't trigger renders when they change. The fix is almost always to use useState; the perf concern was a phantom.
ref can be a callback instead of an object. The callback runs with the DOM node on mount, and with null on unmount:
function MeasuredDiv() {
const setRef = useCallback((node: HTMLDivElement | null) => {
if (node) {
const { width, height } = node.getBoundingClientRect()
// do something with width/height
} else {
// node is null — unmount cleanup
}
}, [])
return <div ref={setRef} />
}Use a ref callback when:
useEffect for this — runs synchronously during commit).composeRefs helper that calls each).Composing refs is a common compound-component need:
function composeRefs<T>(...refs: (React.Ref<T> | undefined)[]) {
return (node: T | null) => {
for (const ref of refs) {
if (typeof ref === 'function') ref(node)
else if (ref) (ref as React.MutableRefObject<T | null>).current = node
}
}
}
// Usage in a Slot-style component:
<button ref={composeRefs(forwardedRef, internalRef)} />Libraries like Radix UI ship composeRefs as part of their primitives because compound components routinely need to merge a consumer's ref with the library's internal ref.
forwardRef — Pre-React-19On React 18 and earlier, a function component cannot accept a ref prop directly — refs are special and need forwardRef to opt in:
import { forwardRef } from 'react'
const StyledButton = forwardRef<HTMLButtonElement, ButtonProps>(
function StyledButton(props, ref) {
return <button ref={ref} className="my-styled-button" {...props} />
},
)The consumer can now do <StyledButton ref={myRef} /> and myRef.current will be the underlying <button> element. Without forwardRef, the same code would warn that "Function components cannot be given refs."
In React 19, ref became a normal prop for function components. forwardRef is no longer required for new code:
// React 19
function StyledButton({ ref, ...props }: ButtonProps & { ref?: React.Ref<HTMLButtonElement> }) {
return <button ref={ref} className="my-styled-button" {...props} />
}forwardRef still works on React 19 for backward compatibility, but the React team's guidance is to use ref-as-prop for new code. Codemods exist to migrate existing forwardRef usage.
The change does not affect:
React.createRef and instance refs).useImperativeHandle — still works the same way; the second argument to the function component is no longer the ref, so reach for the ref prop directly instead.useImperativeHandle — The Controlled Imperative SurfaceWhen a parent legitimately needs to call a method on a child (rare but real cases: focus an input from outside, scroll a virtualized list, open a modal), useImperativeHandle exposes a controlled object via the ref instead of the raw DOM node:
type ModalHandle = { open: () => void; close: () => void }
const Modal = forwardRef<ModalHandle, ModalProps>(function Modal(props, ref) {
const [isOpen, setIsOpen] = useState(false)
useImperativeHandle(ref, () => ({
open: () => setIsOpen(true),
close: () => setIsOpen(false),
}), [])
return isOpen ? <div role="dialog">{props.children}</div> : null
})
// Parent:
function Page() {
const modalRef = useRef<ModalHandle>(null)
return (
<>
<button onClick={() => modalRef.current?.open()}>Open</button>
<Modal ref={modalRef}>...</Modal>
</>
)
}Three discipline rules for useImperativeHandle:
useImperativeHandle are better expressed as a isOpen prop with an onOpenChange callback — that puts the parent in control via the normal data flow.The legitimate use cases are narrow: focus management on a wrapped input, scrolling a virtualized list to an index, triggering an animation on a complex sub-tree. Most other "imperative" needs reveal a state-design problem.
Compound components (Radix UI, Headless UI, Reach UI, shadcn/ui) expose primitive parts (Tabs.Root, Tabs.List, Tabs.Trigger, Tabs.Content) that internally wrap real DOM elements. Consumers expect to forward refs to those underlying elements for focus, measurement, animation, and integration:
// Consumer code that expects ref forwarding to work:
const triggerRef = useRef<HTMLButtonElement>(null)
useEffect(() => { triggerRef.current?.focus() }, [])
<Tabs.Trigger ref={triggerRef} value="settings">Settings</Tabs.Trigger>For this to work, every layer between the consumer and the underlying <button> must forward the ref. Radix accomplishes this with a Slot component that merges a ref onto the rendered child:
// Sketch of how Radix's Slot pattern propagates refs through layers
<Tabs.Trigger> {/* accepts ref, forwards via Slot */}
<Slot ref={mergedRef}> {/* merges consumer ref with library internal ref */}
<button>...</button> {/* receives the merged ref */}
</Slot>
</Tabs.Trigger>The design rule for authoring compound components: every primitive part must forward refs to its DOM node. Failing to do this breaks consumer focus management, breaks third-party integrations that need DOM access, and produces "ref is null" bugs that are hard to diagnose because the chain of components looks correct.
A compound-component library that doesn't forward refs through every primitive is not finished.
Refs only work in Client Components. A ref cannot:
useRef).Server Components produce HTML on the server; there is no DOM node to point at and no client-side runtime to hold the mutable container. If you need a ref, the component (or at least the part of the tree containing the ref) must be a Client Component marked with 'use client'. The broader boundary mechanics belong to client-server-boundary; the consequence for refs is the constraint stated here.
| Anti-pattern | Why it's wrong | Fix |
|---|---|---|
| Using a ref to store form input values to "avoid re-renders" | Reading the ref during render returns stale data; controlled inputs need state | Use useState — the perf concern was a phantom for almost all forms |
Reading ref.current during render | The ref may be null on first render; render-time mutation breaks the render contract | Read refs inside effects, event handlers, or callbacks — never during render |
Mutating a ref during render (ref.current = newValue inline) | Strict mode double-renders cause inconsistent state | Mutate inside effects, event handlers, or initial render guards (if (ref.current === null)) |
useImperativeHandle exposing the whole internal API surface | Couples parent to internals; breaks encapsulation | Expose the minimum; prefer props + callbacks; reserve imperative for genuine escape-hatch needs |
| Compound-component primitive that doesn't forward refs | Consumer focus, measurement, third-party integration all break | Every primitive forwards refs to its underlying DOM element |
Storing a DOM node in useState | Triggers re-render on every reconcile pass; pointless work | useRef for DOM nodes; or ref callback if mount/unmount hook is needed |
| Passing a ref through a Server Component | Refs are not serializable across the boundary | Mark the wrapper as 'use client' so the ref flows in client-only space |
| Forgetting to clean up a ref-held resource (interval, observer, third-party instance) | Memory leak; observer fires after unmount | Clean up in the same effect or in the ref callback's node === null branch |
Using useRef for a value derived from props | Stale closure; the value never updates | Either derive it inline in render (no ref needed) or sync via effect (useEffect(() => { ref.current = derived }, [derived])) — but the sync pattern is usually a smell |
Replacing all forwardRef with ref-as-prop on a React 18 codebase | Breaks until React 19 is shipped | Migrate when React 19 is the active version; forwardRef still works on 18 |
After applying this skill, verify:
ref.current is only read inside effects, event handlers, or callbacks — never during render.useImperativeHandle exposes the minimum surface needed; props + callbacks were considered first.'use client' boundary as a prop from a Server Component.forwardRef is retained for backward compat or migrated via codemod.useState(domNode) — DOM nodes go in refs, not state.composeRefs helper rather than ad-hoc merging.useRef. The hook reference, with the ref-vs-state decision tree.forwardRef (deprecated as of React 19) and the ref-as-prop migration note. The pre- and post-React-19 API.useImperativeHandle. The controlled imperative surface API.useImperativeHandle).| Instead of this skill | Use | Why |
|---|---|---|
| The broader hook discipline — Rules of Hooks, dependency arrays, custom hooks, You Might Not Need an Effect | hooks-patterns | hooks-patterns owns the hook model; this skill covers the ref family specifically. |
| The cross-product component-layering question — primitives vs composites, headless vs styled, controlled vs uncontrolled | component-architecture | component-architecture owns the layering; this skill covers the ref-forwarding mechanics that compound primitives need. |
| State location and ownership decisions for server / client / URL / persistent state | state-management | state-management owns state; this skill owns the not-state primitive and the decision boundary between them. |
The serialization and directive mechanics of 'use client' and 'use server' | client-server-boundary | client-server-boundary owns the boundary; this skill notes the constraint that refs can't cross it. |
| Form-state design — validation, accessibility, microcopy, layout | form-ux-architecture | form-ux-architecture owns forms; refs may appear in forms (focus management, uncontrolled inputs) but the broader design belongs there. |
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.