agentsclimarketplace

Ref patterns

Skill jacob-balslev/skill-graph/marketplace/skills/ref-patterns

Use when designing or reviewing React ref usage: refs as mutable handles that survive renders without triggering them, useRef for DOM access and instance values, ref callbacks for mount/unmount hooks, forwardRef and React 19 ref-as-prop, useImperativeHandle for controlled imperative APIs, and ref forwarding through compound-component primitives such as Radix Slot. Use for focus, measurement, animation, third-party DOM integration, and sparse imperative APIs; never as a substitute for reactive state. Do NOT use for the broader hook discipline (use react-hooks-patterns), state ownership decisions (use state-management), component-layering strategy (use component-architecture), Client/Server serialization boundaries (use client-server-boundary), or form validation UX (use form-ux-architecture). Do NOT use for design the Rules of Hooks and dependency-array discipline for useEffect (use hooks-patterns).From its SKILL.md

Install
npx -y skills add jacob-balslev/skill-graph --skill ref-patterns

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its file declares

Copied from the file, not written here

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

29.5 KB, ~5.2k tokens by cl100k_base, as published. Nobody here has run it

Ref Patterns

Concept of the skill

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.

Coverage

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.

Philosophy of the skill

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:

  • A DOM node is an external resource that React produces but does not own. A ref gives the component a stable handle to that node for focus, measurement, animation, or third-party integration.
  • An interval id, a 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.
  • A child component sometimes needs to expose an imperative surface (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 Primitive

import { 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.

The Ref / State Decision

SymptomRight primitive
The value drives what's rendereduseState
The value affects an effect's behavior but isn't shownuseRef (unless the effect needs to re-run when it changes, then useState)
You want a re-render when this changesuseState
You don't want a re-render when this changesuseRef
You're storing a DOM nodeuseRef (never useState — would trigger a re-render on every reconciliation pass)
You're storing a setTimeout / setInterval iduseRef
You're tracking "previous value" or "latest args" for an effectuseRef
You're caching an expensive value that depends on inputsuseMemo (not a ref)
You're holding form-input stateuseState 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 Callbacks

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:

  • You need to run code at the exact moment the DOM node mounts or unmounts (more reliable than useEffect for this — runs synchronously during commit).
  • You need to attach the same ref to multiple elements conditionally (the callback is called with each).
  • You need to compose multiple refs onto one element (write a 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-19

On 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."

React 19 — Ref as Prop

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:

  • Class components (still need React.createRef and instance refs).
  • The fundamentals — refs still flow from consumer to provider; what changed is the API for accepting them, not the model.
  • 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 Surface

When 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:

  1. Expose the minimum surface. Don't return the full set of internal methods; return only what the parent needs. The parent's ref is a public API; treat it as one.
  2. Don't expose state read functions. If the parent needs to read state, lift state up. Reading via an imperative method makes the parent's render unreactive to that state.
  3. Default to props. Most cases that look like they need 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.

Ref Forwarding in Compound Components

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 and the Client/Server Boundary

Refs only work in Client Components. A ref cannot:

  • Be created inside a Server Component (no useRef).
  • Be passed as a prop from a Server Component to a Client Component (refs are not serializable).
  • Be returned from a Server Action (same reason).

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.

Common Anti-Patterns

Anti-patternWhy it's wrongFix
Using a ref to store form input values to "avoid re-renders"Reading the ref during render returns stale data; controlled inputs need stateUse useState — the perf concern was a phantom for almost all forms
Reading ref.current during renderThe ref may be null on first render; render-time mutation breaks the render contractRead 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 stateMutate inside effects, event handlers, or initial render guards (if (ref.current === null))
useImperativeHandle exposing the whole internal API surfaceCouples parent to internals; breaks encapsulationExpose the minimum; prefer props + callbacks; reserve imperative for genuine escape-hatch needs
Compound-component primitive that doesn't forward refsConsumer focus, measurement, third-party integration all breakEvery primitive forwards refs to its underlying DOM element
Storing a DOM node in useStateTriggers re-render on every reconcile pass; pointless workuseRef for DOM nodes; or ref callback if mount/unmount hook is needed
Passing a ref through a Server ComponentRefs are not serializable across the boundaryMark 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 unmountClean up in the same effect or in the ref callback's node === null branch
Using useRef for a value derived from propsStale closure; the value never updatesEither 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 codebaseBreaks until React 19 is shippedMigrate when React 19 is the active version; forwardRef still works on 18

Verification

After applying this skill, verify:

  • Every ref is either a DOM ref or a mutable instance value — never a substitute for state that drives the UI.
  • ref.current is only read inside effects, event handlers, or callbacks — never during render.
  • Compound-component primitives forward refs to their underlying DOM elements.
  • useImperativeHandle exposes the minimum surface needed; props + callbacks were considered first.
  • No ref is being passed across the 'use client' boundary as a prop from a Server Component.
  • Ref-held resources (intervals, observers, third-party instances) have cleanup paths in effects or ref-callback null branches.
  • On React 19, new code uses ref-as-prop; existing forwardRef is retained for backward compat or migrated via codemod.
  • No useState(domNode) — DOM nodes go in refs, not state.
  • Composed refs (multiple consumers attaching to one element) use a composeRefs helper rather than ad-hoc merging.

Grounding Sources

Do NOT Use When

Instead of this skillUseWhy
The broader hook discipline — Rules of Hooks, dependency arrays, custom hooks, You Might Not Need an Effecthooks-patternshooks-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 uncontrolledcomponent-architecturecomponent-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 statestate-managementstate-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-boundaryclient-server-boundary owns the boundary; this skill notes the constraint that refs can't cross it.
Form-state design — validation, accessibility, microcopy, layoutform-ux-architectureform-ux-architecture owns forms; refs may appear in forms (focus management, uncontrolled inputs) but the broader design belongs there.

Skill Graph context

<!-- skill-graph-context:start (generated — do not edit by hand) -->

Classification

  • Subject: frontend-engineering
  • Public: true
  • Domain: engineering/frontend
  • Scope: Designing and reviewing React ref usage — refs (mutable handle that survives renders without triggering them) vs state (reactive value), useRef for DOM access and mutable instance values, ref callbacks for mount/unmount hooks, forwardRef (and the React 19 ref-as-prop change that retires it for new code), useImperativeHandle for a controlled imperative surface, ref forwarding through compound-component primitives (Radix Slot / Headless UI), and the rule that refs are an escape hatch (DOM access, non-React DOM libraries, focus, animation, measurement, imperative APIs) never a substitute for state. Portable across React codebases; principle-grounded, not repo-bound. Excludes broader hook discipline (hooks-patterns), component layering (component-architecture), state location and ownership (state-management), the use-client/use-server boundary (client-server-boundary), and form-state/validation (form-ux-architecture).

When to use

  • design a Modal component that exposes open() and close() to the parent via useImperativeHandle so the parent can trigger it imperatively without lifting full state
  • forward a ref through a styled Button wrapper to the underlying <button> element using React 19 ref-as-prop (or forwardRef on React 18 and earlier)
  • integrate a third-party chart library that needs a DOM container by handing it a ref callback that initializes on mount and tears down on unmount
  • replace a useState that nobody reads in render with a useRef because the value drives an imperative side effect (interval id, latest-args closure) not the render output
  • audit a component that uses a ref to read 'current form values' instead of reading from controlled state — usually a sign the wrong primitive was chosen
  • Triggers: how do I focus an input on mount, how do I pass a ref through a wrapper component, do I still need forwardRef in React 19, when should I use a ref instead of state, how do I expose a method like open or close to the parent, how do I measure a DOM element, why is my ref.current null on first render, how do I integrate a non-React DOM library

Not for

  • design the Rules of Hooks and dependency-array discipline for useEffect (use hooks-patterns)
  • decide whether the form state lives in URL, server, client, or persistent storage (use state-management)
  • design the headless-vs-styled layering of a component library (use component-architecture)
  • explain how 'use client' marks a component boundary (use client-server-boundary)
  • design the validation-state UX of an input (use form-ux-architecture)
  • design the layering and API surface of a cross-product component library (use component-architecture)
  • design the validation states, layout, and microcopy of a form (use form-ux-architecture)
  • Owned by hooks-patterns
  • Owned by state-management: the location and ownership decisions for the four kinds of state (server / client UI / URL / persistent)
  • Owned by client-server-boundary: the serialization and directive mechanics

Related skills

  • Verify with: code-review, hooks-patterns
  • Related: hooks-patterns, component-architecture, state-management, client-server-boundary, form-ux-architecture

Concept

  • Mental model: |
  • Purpose: |
  • Boundary: |
  • Analogy: A ref is to a React component what a static local variable is to a C function — it persists across calls (renders), reading it does not make the function 'depend on' it, writing to it does not change the function's signature or trigger any caller-visible event, and its single purpose is to hold the state that is not part of the function's interface. State, by contrast, is to the component what the function's return value is to the caller: every read participates in the contract, every change requires a re-evaluation.
  • Common misconception: |

Keywords

  • useRef hook, forwardRef, useImperativeHandle, ref callback, DOM ref React, ref forwarding compound component, React 19 ref as prop, mutable ref vs state, current property ref, Radix Slot ref forwarding
<!-- skill-graph-context:end -->

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 326,629. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.