
React Syntax Refs
- 11 installs
- 6 repo stars
- Updated July 8, 2026
- openaec-foundation/react-claude-skill-package
Helps with frontend development tasks.
About
react-syntax-refs is a Claude Code skill for frontend development. It helps solo builders move faster with AI-assisted development.
- react-syntax-refs
- Frontend Development
- AI-coding skill
React Syntax Refs by the numbers
- 11 all-time installs (skills.sh)
- Ranked #1,658 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/openaec-foundation/react-claude-skill-package --skill react-syntax-refsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 11 |
|---|---|
| repo stars | ★ 6 |
| Last updated | July 8, 2026 |
| Repository | openaec-foundation/react-claude-skill-package ↗ |
What it does
Helps with frontend development tasks.
Files
react-syntax-refs
Quick Reference
Ref Hooks and APIs
| API | Purpose | React 18 | React 19 |
|---|---|---|---|
useRef<T> | Hold mutable value or DOM reference | Yes | Yes |
forwardRef | Pass ref through component | Required | Deprecated (ref is a prop) |
useImperativeHandle | Expose limited API to parent | With forwardRef | With ref prop |
| Callback refs | Function-based ref assignment | Yes | Yes + cleanup return |
TypeScript Signatures
// useRef — DOM element
const ref = useRef<HTMLInputElement>(null);
// useRef — mutable value (no null in generic = mutable)
const countRef = useRef<number>(0);
// useImperativeHandle
useImperativeHandle<HandleType>(ref, () => ({ method() {} }), [deps]);Critical Warnings
NEVER read or write ref.current during rendering. The only exception is lazy initialization (if (ref.current === null) ref.current = new Thing()). Reading/writing refs during render breaks component purity and causes unpredictable behavior.
NEVER use forwardRef in new React 19 code. Pass ref as a regular prop instead. forwardRef is deprecated in React 19.
NEVER expose the entire DOM node via useImperativeHandle. ALWAYS expose only the specific methods the parent needs. Exposing the full node breaks encapsulation.
ALWAYS type useRef with the correct generic. For DOM refs, ALWAYS initialize with null and type as useRef<HTMLElement>(null). For mutable values, initialize with the actual value.
---
Decision Tree: Ref vs State
Need to store a value across renders?
|
+-- Does changing this value need to update the UI?
| YES --> Use useState or useReducer
| NO --> Use useRef
|
+-- Is this a DOM element reference?
| YES --> Use useRef<HTMLElement>(null), attach via ref prop
|
+-- Is this a timer ID, previous value, or instance variable?
YES --> Use useRef<T>(initialValue)Rule: If the value is used ONLY in event handlers or effects and NEVER in JSX output, use useRef. If it appears in rendered output, use useState.
---
useRef: DOM Access
import { useRef } from "react";
function TextInput(): JSX.Element {
const inputRef = useRef<HTMLInputElement>(null);
function handleClick(): void {
inputRef.current?.focus();
}
return (
<>
<input ref={inputRef} type="text" />
<button onClick={handleClick}>Focus</button>
</>
);
}TypeScript Generics for DOM Refs
| Element | Generic Type |
|---|---|
<input> | useRef<HTMLInputElement>(null) |
<div> | useRef<HTMLDivElement>(null) |
<canvas> | useRef<HTMLCanvasElement>(null) |
<video> | useRef<HTMLVideoElement>(null) |
<form> | useRef<HTMLFormElement>(null) |
<svg> | useRef<SVGSVGElement>(null) |
ALWAYS initialize DOM refs with null. The element is not available until after mount.
---
useRef: Mutable Values
import { useRef, useEffect } from "react";
function Timer(): JSX.Element {
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const renderCountRef = useRef<number>(0);
useEffect(() => {
intervalRef.current = setInterval(() => {
renderCountRef.current += 1;
}, 1000);
return () => {
if (intervalRef.current !== null) {
clearInterval(intervalRef.current);
}
};
}, []);
return <div>Timer running</div>;
}Key behavior: Changing .current does NOT trigger a re-render. React is not aware of ref mutations. The same object reference persists across every render.
---
forwardRef (React 18) vs ref Prop (React 19)
React 18: forwardRef Required
import { forwardRef, type Ref } from "react";
interface InputProps {
label: string;
}
const LabeledInput = forwardRef<HTMLInputElement, InputProps>(
function LabeledInput(props, ref) {
return (
<label>
{props.label}
<input ref={ref} />
</label>
);
}
);
// Usage
const ref = useRef<HTMLInputElement>(null);
<LabeledInput ref={ref} label="Name" />;React 19: ref as Regular Prop
import { type Ref } from "react";
interface InputProps {
label: string;
ref?: Ref<HTMLInputElement>;
}
function LabeledInput({ label, ref }: InputProps): JSX.Element {
return (
<label>
{label}
<input ref={ref} />
</label>
);
}
// Usage — identical
const ref = useRef<HTMLInputElement>(null);
<LabeledInput ref={ref} label="Name" />;In React 19, ref is destructured from props like any other prop. No wrapper needed.
---
useImperativeHandle
Exposes a limited, custom API to the parent instead of the raw DOM node.
React 19 Pattern
import { useRef, useImperativeHandle, type Ref } from "react";
interface TextInputHandle {
focus: () => void;
scrollIntoView: () => void;
}
interface TextInputProps {
placeholder?: string;
ref?: Ref<TextInputHandle>;
}
function TextInput({ placeholder, ref }: TextInputProps): JSX.Element {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus() {
inputRef.current?.focus();
},
scrollIntoView() {
inputRef.current?.scrollIntoView({ behavior: "smooth" });
},
}), []);
return <input ref={inputRef} placeholder={placeholder} />;
}
// Parent usage
function Parent(): JSX.Element {
const textInputRef = useRef<TextInputHandle>(null);
return (
<>
<TextInput ref={textInputRef} placeholder="Type here" />
<button onClick={() => textInputRef.current?.focus()}>
Focus Input
</button>
</>
);
}React 18 Pattern
import { forwardRef, useRef, useImperativeHandle } from "react";
const TextInput = forwardRef<TextInputHandle, TextInputProps>(
function TextInput(props, ref) {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus() { inputRef.current?.focus(); },
}), []);
return <input ref={inputRef} placeholder={props.placeholder} />;
}
);ALWAYS define a TypeScript interface for the handle type. This provides compile-time safety for the parent component.
---
Callback Refs
A function passed as ref receives the DOM node on mount and null on unmount.
Basic Callback Ref
function MeasuredBox(): JSX.Element {
const [height, setHeight] = useState<number>(0);
const measuredRef = (node: HTMLDivElement | null): void => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
};
return <div ref={measuredRef}>Height: {height}px</div>;
}React 19: Ref Cleanup Function
In React 19, callback refs can return a cleanup function (like effects):
function VideoPlayer(): JSX.Element {
const onRefChange = (node: HTMLVideoElement | null): (() => void) | void => {
if (node !== null) {
node.play();
return () => {
node.pause(); // Cleanup when ref detaches
};
}
};
return <video ref={onRefChange} src="/video.mp4" />;
}React 18: Callback ref receives null on unmount. No return value. React 19: Callback ref MAY return a cleanup function. React calls it when the node detaches. Returning a cleanup means the ref does NOT receive null on unmount.
---
Ref Read/Write Rules
| Context | Read .current | Write .current |
|---|---|---|
| Event handlers | ALLOWED | ALLOWED |
Effects (useEffect, useLayoutEffect) | ALLOWED | ALLOWED |
| During render | FORBIDDEN | FORBIDDEN |
| Lazy initialization in render | ALLOWED (if (ref.current === null)) | ALLOWED (one-time init) |
Lazy Initialization Pattern
function getPlayer(): VideoPlayer {
// Expensive to create — initialize once
return new VideoPlayer();
}
function VideoComponent(): JSX.Element {
const playerRef = useRef<VideoPlayer | null>(null);
// Safe: one-time initialization check during render
if (playerRef.current === null) {
playerRef.current = getPlayer();
}
return <div>{/* use playerRef.current */}</div>;
}---
Reference Links
- references/examples.md -- Working ref patterns with full TypeScript types
- references/anti-patterns.md -- Common ref mistakes and corrections
Official Sources
- https://react.dev/reference/react/useRef
- https://react.dev/reference/react/useImperativeHandle
- https://react.dev/reference/react/forwardRef
- https://react.dev/learn/referencing-values-with-refs
- https://react.dev/learn/manipulating-the-dom-with-refs
react-syntax-refs — Anti-Patterns
Common ref mistakes with explanations and corrections. Every anti-pattern includes WHY it fails and the correct alternative.
---
AP-001: Reading ref.current During Render
// WRONG — breaks component purity
function Display(): JSX.Element {
const ref = useRef<number>(0);
ref.current += 1; // Writing during render
return <p>Renders: {ref.current}</p>; // Reading during render
}Why it fails: React may call render functions multiple times (StrictMode, concurrent features, Suspense retries). Reading/writing refs during render produces inconsistent results because React does not track ref mutations.
// CORRECT — use state for values that affect render output
function Display(): JSX.Element {
const [renderCount, setRenderCount] = useState<number>(0);
useEffect(() => {
setRenderCount((prev) => prev + 1);
}, []);
return <p>Renders: {renderCount}</p>;
}Rule: If the value appears in JSX, ALWAYS use useState. Refs are for values that do NOT affect rendered output.
---
AP-002: Using forwardRef in React 19
// WRONG in React 19 — forwardRef is deprecated
const MyInput = forwardRef<HTMLInputElement, Props>((props, ref) => {
return <input ref={ref} {...props} />;
});Why it fails: forwardRef adds unnecessary complexity in React 19 where ref is a regular prop. It still works but is deprecated and adds a wrapper layer.
// CORRECT in React 19 — ref as prop
function MyInput({ ref, ...props }: Props & { ref?: Ref<HTMLInputElement> }): JSX.Element {
return <input ref={ref} {...props} />;
}Rule: In React 19 projects, NEVER use forwardRef. In React 18 projects or libraries supporting both, forwardRef is still required.
---
AP-003: Exposing Full DOM Node via useImperativeHandle
// WRONG — exposes entire DOM API
function CustomInput({ ref }: { ref?: Ref<HTMLInputElement> }): JSX.Element {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => inputRef.current!, []);
return <input ref={inputRef} />;
}Why it fails: The parent receives the full HTMLInputElement API, breaking encapsulation. The child component cannot change its internal structure without breaking consumers. The non-null assertion (!) can cause runtime errors if the ref is not yet attached.
// CORRECT — expose only what the parent needs
interface CustomInputHandle {
focus: () => void;
clear: () => void;
}
function CustomInput({ ref }: { ref?: Ref<CustomInputHandle> }): JSX.Element {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus() { inputRef.current?.focus(); },
clear() { if (inputRef.current) inputRef.current.value = ""; },
}), []);
return <input ref={inputRef} />;
}Rule: ALWAYS define a TypeScript interface for the handle. ALWAYS expose specific methods, NEVER the raw node.
---
AP-004: Creating Callback Ref Inline Without Stability
// WRONG — new function every render, causes detach/reattach cycle
function Measurer(): JSX.Element {
const [height, setHeight] = useState<number>(0);
return (
<div
ref={(node: HTMLDivElement | null) => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}}
>
Height: {height}
</div>
);
}Why it fails: The inline function creates a new reference every render. React detaches the old ref (calls it with null) and attaches the new one (calls it with the node) on every render. This causes unnecessary DOM measurements and potential flicker. If setHeight triggers a re-render, it creates an infinite loop.
// CORRECT — stable callback ref with useCallback
function Measurer(): JSX.Element {
const [height, setHeight] = useState<number>(0);
const measuredRef = useCallback((node: HTMLDivElement | null) => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []);
return (
<div ref={measuredRef}>Height: {height}</div>
);
}Rule: ALWAYS wrap callback refs in useCallback when the callback triggers state updates or performs expensive operations.
---
AP-005: Using useRef Where useState Is Needed
// WRONG — UI never updates
function Counter(): JSX.Element {
const countRef = useRef<number>(0);
function increment(): void {
countRef.current += 1;
// Component does NOT re-render
}
return (
<div>
<p>Count: {countRef.current}</p> {/* Always shows 0 */}
<button onClick={increment}>+1</button>
</div>
);
}Why it fails: Mutating ref.current does not trigger a re-render. The displayed value never updates.
// CORRECT — use state for rendered values
function Counter(): JSX.Element {
const [count, setCount] = useState<number>(0);
function increment(): void {
setCount((prev) => prev + 1);
}
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+1</button>
</div>
);
}Rule: If the value appears in JSX output, ALWAYS use useState. Use useRef ONLY for values that do not affect what is rendered.
---
AP-006: Forgetting null Check on DOM Ref
// WRONG — runtime error if ref not attached yet
function AutoScroll(): JSX.Element {
const divRef = useRef<HTMLDivElement>(null);
useEffect(() => {
divRef.current.scrollTop = divRef.current.scrollHeight; // TypeError if null
}, []);
return <div ref={divRef}>Content</div>;
}Why it fails: DOM refs are null until after the component mounts and the ref attaches. Accessing properties on null throws a TypeError.
// CORRECT — always null-check DOM refs
function AutoScroll(): JSX.Element {
const divRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (divRef.current !== null) {
divRef.current.scrollTop = divRef.current.scrollHeight;
}
}, []);
return <div ref={divRef}>Content</div>;
}Rule: ALWAYS null-check DOM refs before accessing properties. Use optional chaining (ref.current?.method()) or explicit null guards.
---
AP-007: Using String Refs (Legacy)
// WRONG — string refs are removed in React 19
class OldComponent extends React.Component {
render() {
return <input ref="myInput" />;
}
}Why it fails: String refs were removed in React 19 and have been deprecated since React 16.3. They have performance issues and do not work with function components.
// CORRECT — use useRef in function components
function ModernComponent(): JSX.Element {
const inputRef = useRef<HTMLInputElement>(null);
return <input ref={inputRef} />;
}Rule: NEVER use string refs. ALWAYS use useRef or callback refs.
---
AP-008: Missing Dependencies in useImperativeHandle
// WRONG — stale closure over inputRef
function SearchInput({ ref }: { ref?: Ref<SearchHandle> }): JSX.Element {
const inputRef = useRef<HTMLInputElement>(null);
const [query, setQuery] = useState<string>("");
useImperativeHandle(ref, () => ({
getValue() { return query; }, // Captures initial query value
focus() { inputRef.current?.focus(); },
}), []); // Missing 'query' dependency
return <input ref={inputRef} value={query} onChange={(e) => setQuery(e.target.value)} />;
}Why it fails: The getValue method captures the initial value of query due to the empty dependency array. The parent always receives the stale initial value.
// CORRECT — include dependencies
useImperativeHandle(ref, () => ({
getValue() { return query; },
focus() { inputRef.current?.focus(); },
}), [query]); // query included in depsRule: ALWAYS include all reactive values used inside createHandle in the dependency array. The linter (eslint-plugin-react-hooks) catches this.
---
AP-009: Returning Cleanup from Callback Ref in React 18
// WRONG in React 18 — cleanup return not supported
function TrackedElement(): JSX.Element {
const trackRef = (node: HTMLDivElement | null): (() => void) => {
if (node !== null) {
analytics.trackImpression(node);
return () => analytics.untrackImpression(node); // Ignored in React 18
}
return () => {};
};
return <div ref={trackRef}>Tracked</div>;
}Why it fails: React 18 does not support cleanup return values from callback refs. The returned function is silently ignored. React 18 passes null to the callback on unmount instead.
// CORRECT for React 18 — handle null as cleanup signal
function TrackedElement(): JSX.Element {
const nodeRef = useRef<HTMLDivElement | null>(null);
const trackRef = useCallback((node: HTMLDivElement | null): void => {
if (nodeRef.current !== null) {
analytics.untrackImpression(nodeRef.current); // Cleanup previous
}
if (node !== null) {
analytics.trackImpression(node); // Setup new
}
nodeRef.current = node;
}, []);
return <div ref={trackRef}>Tracked</div>;
}Rule: In React 18, ALWAYS handle cleanup in the null branch of callback refs. In React 19, you MAY return a cleanup function instead.
react-syntax-refs — Examples
Extended ref patterns with full TypeScript types. All examples verified against React 18.x and 19.x official documentation.
---
Scrolling to a List Item with Callback Refs
Using a Map to track multiple DOM nodes without knowing the count at compile time:
import { useRef } from "react";
interface CatItem {
id: string;
imageUrl: string;
}
function CatGallery({ cats }: { cats: CatItem[] }): JSX.Element {
const itemsRef = useRef<Map<string, HTMLLIElement>>(new Map());
function scrollToId(itemId: string): void {
const node = itemsRef.current.get(itemId);
node?.scrollIntoView({ behavior: "smooth", block: "nearest" });
}
return (
<ul>
{cats.map((cat) => (
<li
key={cat.id}
ref={(node: HTMLLIElement | null) => {
if (node !== null) {
itemsRef.current.set(cat.id, node);
} else {
itemsRef.current.delete(cat.id);
}
}}
>
<img src={cat.imageUrl} alt={`Cat ${cat.id}`} />
</li>
))}
</ul>
);
}When to use: ALWAYS use a Map-based callback ref when you need refs to a dynamic list of elements. A single useRef can only hold one value.
---
Previous Value Tracking
import { useRef, useEffect } from "react";
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>(undefined);
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
// Usage
function Counter({ count }: { count: number }): JSX.Element {
const prevCount = usePrevious(count);
return (
<p>
Now: {count}, Before: {prevCount ?? "N/A"}
</p>
);
}---
Stable Callback with useRef
Storing the latest version of a callback without causing effect re-runs:
import { useRef, useEffect, useCallback } from "react";
function useLatestCallback<T extends (...args: any[]) => any>(
callback: T
): T {
const ref = useRef<T>(callback);
useEffect(() => {
ref.current = callback;
}, [callback]);
// Return a stable wrapper that always calls the latest version
return useCallback(
((...args: Parameters<T>) => ref.current(...args)) as T,
[]
);
}---
Composing Multiple Refs
When a single element needs multiple refs (e.g., a library ref and a local ref):
import { useRef, useCallback, type Ref, type RefCallback } from "react";
function useMergedRefs<T>(
...refs: Array<Ref<T> | undefined>
): RefCallback<T> {
return useCallback((node: T | null) => {
refs.forEach((ref) => {
if (ref === null || ref === undefined) return;
if (typeof ref === "function") {
ref(node);
} else {
(ref as React.MutableRefObject<T | null>).current = node;
}
});
}, refs);
}
// Usage
function InputWithLibrary({
ref,
}: {
ref?: Ref<HTMLInputElement>;
}): JSX.Element {
const localRef = useRef<HTMLInputElement>(null);
const mergedRef = useMergedRefs(ref, localRef);
return <input ref={mergedRef} />;
}---
useImperativeHandle with Complex API
Exposing a rich imperative API for a modal component:
import { useRef, useImperativeHandle, useState, type Ref } from "react";
interface ModalHandle {
open: () => void;
close: () => void;
toggle: () => void;
isOpen: () => boolean;
}
interface ModalProps {
title: string;
children: React.ReactNode;
ref?: Ref<ModalHandle>;
}
function Modal({ title, children, ref }: ModalProps): JSX.Element | null {
const [visible, setVisible] = useState<boolean>(false);
useImperativeHandle(ref, () => ({
open() {
setVisible(true);
},
close() {
setVisible(false);
},
toggle() {
setVisible((prev) => !prev);
},
isOpen() {
return visible;
},
}), [visible]);
if (!visible) return null;
return (
<div role="dialog" aria-label={title}>
<h2>{title}</h2>
{children}
<button onClick={() => setVisible(false)}>Close</button>
</div>
);
}
// Parent usage
function App(): JSX.Element {
const modalRef = useRef<ModalHandle>(null);
return (
<>
<button onClick={() => modalRef.current?.open()}>Show Modal</button>
<Modal ref={modalRef} title="Confirm">
<p>Are you sure?</p>
</Modal>
</>
);
}---
Conditional Ref Assignment
Using callback refs to conditionally track elements:
import { useState, useCallback } from "react";
function AutofocusOnExpand(): JSX.Element {
const [expanded, setExpanded] = useState<boolean>(false);
const inputCallbackRef = useCallback((node: HTMLInputElement | null) => {
if (node !== null) {
// Focus immediately when the input mounts
node.focus();
}
}, []);
return (
<div>
<button onClick={() => setExpanded((prev) => !prev)}>
{expanded ? "Collapse" : "Expand"}
</button>
{expanded && <input ref={inputCallbackRef} placeholder="Auto-focused" />}
</div>
);
}When to use: ALWAYS use a callback ref when you need to perform an action (focus, measure, animate) at the exact moment a DOM node attaches.
---
React 19 Ref Cleanup for Intersection Observer
function LazyImage({ src, alt }: { src: string; alt: string }): JSX.Element {
const [isVisible, setIsVisible] = useState<boolean>(false);
// React 19: return cleanup from callback ref
const observeRef = (node: HTMLDivElement | null): (() => void) | void => {
if (node === null) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
},
{ threshold: 0.1 }
);
observer.observe(node);
return () => {
observer.disconnect(); // Cleanup on detach
};
};
return (
<div ref={observeRef}>
{isVisible ? <img src={src} alt={alt} /> : <div>Loading...</div>}
</div>
);
}---
DOM Measurement with useLayoutEffect
When you need accurate measurements before the browser paints:
import { useRef, useState, useLayoutEffect } from "react";
interface Dimensions {
width: number;
height: number;
}
function MeasuredContainer({
children,
}: {
children: React.ReactNode;
}): JSX.Element {
const containerRef = useRef<HTMLDivElement>(null);
const [dimensions, setDimensions] = useState<Dimensions>({
width: 0,
height: 0,
});
useLayoutEffect(() => {
if (containerRef.current !== null) {
const { width, height } =
containerRef.current.getBoundingClientRect();
setDimensions({ width, height });
}
}, [children]);
return (
<div ref={containerRef}>
<div>
Size: {dimensions.width}x{dimensions.height}
</div>
{children}
</div>
);
}ALWAYS use useLayoutEffect (not useEffect) when measurements must be accurate before paint. useEffect runs after paint and causes visible flicker.
---
Forwarding Refs Through HOCs (React 18)
import { forwardRef, type ComponentType, type Ref } from "react";
interface WithLoggerProps {
ref?: Ref<HTMLElement>;
}
function withLogger<P extends object>(
WrappedComponent: ComponentType<P>
): ComponentType<P & WithLoggerProps> {
const WithLogger = forwardRef<HTMLElement, P>(
function WithLogger(props, ref) {
console.log("Rendered:", WrappedComponent.displayName);
return <WrappedComponent {...props} ref={ref} />;
}
);
WithLogger.displayName = `WithLogger(${
WrappedComponent.displayName ?? WrappedComponent.name
})`;
return WithLogger;
}In React 19 this HOC pattern is simplified because ref flows through as a normal prop without forwardRef.