com um clique
react-hooks
React hooks mastery
Instalar com Codex ou Claude Copie este prompt, cole no Codex, Claude ou outro assistente e deixe que ele revise a página da skill e instale para você.
Menu
React hooks mastery
Instalar com Codex ou Claude Copie este prompt, cole no Codex, Claude ou outro assistente e deixe que ele revise a página da skill e instale para você.
Baseado na classificação ocupacional SOC
Audit a project against a canon's rules and checklist. Read-only — produces prioritized report without fixing. Works with any canon (nextjs, sql, typescript, etc.).
Lens home base - status, help, and setup
Plan and build a new feature with quality gates.
Simple changes done right. Make the change, clean up after yourself, report what happened.
Review against canons + quality gate, fix findings, verify. Claude-native — no external models.
Plan and improve existing code with quality gates.
| name | react-hooks |
| description | React hooks mastery |
Applying the React team's hooks API correctly when building components that need DOM access, layout measurement, accessible IDs, concurrent rendering, imperative child APIs, or custom hook composition. Auto-triggers on .jsx/.tsx files using useRef, useLayoutEffect, useId, useTransition, useDeferredValue, useImperativeHandle, or forwardRef. Not for basic useState/useEffect (see react-state), not for state management architecture, not for styling.
Refs hold mutable values that persist across renders without triggering re-renders. Two use cases: DOM node access and mutable instance storage.
// DOM ref — focus on mount
function AutoFocusInput() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => { inputRef.current?.focus(); }, []);
return <input ref={inputRef} />;
}
ref.current survives re-renders like state, but changing it does not re-render.
// Not this — state for non-rendered values causes wasted re-renders
function Stopwatch() {
const [intervalId, setIntervalId] = useState<number | null>(null);
// Every setIntervalId call re-renders. The interval ID is never displayed.
}
// This — ref for values that don't affect rendering
function Stopwatch() {
const intervalRef = useRef<number | null>(null);
const [elapsed, setElapsed] = useState(0);
function start() {
intervalRef.current = window.setInterval(() => setElapsed(e => e + 1), 1000);
}
function stop() { window.clearInterval(intervalRef.current!); }
return <span>{elapsed}s</span>;
}
Previous value pattern -- a ref updated in useEffect lags one render behind:
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>(undefined);
useEffect(() => { ref.current = value; });
return ref.current;
}
Rule: Displayed in JSX? Use state. Only read by handlers or effects? Use a ref.
Both run after render. useEffect runs asynchronously after paint. useLayoutEffect runs synchronously before paint, blocking the browser.
// Not this — useEffect causes visible flicker (tooltip at 0,0 then jumps)
function Tooltip({ anchorRef, children }: TooltipProps) {
const [pos, setPos] = useState({ top: 0, left: 0 });
useEffect(() => {
const rect = anchorRef.current!.getBoundingClientRect();
setPos({ top: rect.bottom + 8, left: rect.left });
}, [anchorRef]);
return <div style={pos}>{children}</div>;
}
// This — useLayoutEffect measures before user sees anything
function Tooltip({ anchorRef, children }: TooltipProps) {
const [pos, setPos] = useState({ top: 0, left: 0 });
useLayoutEffect(() => {
const rect = anchorRef.current!.getBoundingClientRect();
setPos({ top: rect.bottom + 8, left: rect.left });
}, [anchorRef]);
return <div style={pos}>{children}</div>;
}
Scroll restoration is another classic case:
function ChatMessages({ messages }: { messages: Message[] }) {
const ref = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
ref.current!.scrollTop = ref.current!.scrollHeight;
}, [messages.length]);
return (
<div ref={ref} style={{ overflow: 'auto', maxHeight: 400 }}>
{messages.map(m => <MessageRow key={m.id} message={m} />)}
</div>
);
}
Rule: Default to useEffect. Switch to useLayoutEffect only when DOM measurement or mutation would cause visible flicker.
useId generates stable IDs that match between server and client. Hand-rolled counters break hydration.
// Not this — IDs mismatch between server and client
let nextId = 0;
function FormField({ label }: { label: string }) {
const id = `field-${nextId++}`;
return <><label htmlFor={id}>{label}</label><input id={id} /></>;
}
// This — stable across server and client
function FormField({ label }: { label: string }) {
const id = useId();
return <><label htmlFor={id}>{label}</label><input id={id} /></>;
}
Derive multiple related IDs from one call:
function PasswordField() {
const id = useId();
return (
<div>
<label htmlFor={`${id}-input`}>Password</label>
<input id={`${id}-input`} type="password" aria-describedby={`${id}-error ${id}-help`} />
<p id={`${id}-help`}>Must be 8+ characters</p>
<p id={`${id}-error`} role="alert">Password too short</p>
</div>
);
}
Rule: Use useId for all htmlFor, aria-describedby, aria-labelledby. Never counters or Math.random().
Marks a state update as low-priority. React keeps the UI responsive while computing the next render in the background.
// Not this — expensive filter blocks the input on every keystroke
function UserSearch({ users }: { users: User[] }) {
const [query, setQuery] = useState('');
const filtered = users.filter(u => u.name.toLowerCase().includes(query.toLowerCase()));
return (
<>
<input value={query} onChange={e => setQuery(e.target.value)} />
<UserList users={filtered} />
</>
);
}
// This — transition keeps input responsive
function UserSearch({ users }: { users: User[] }) {
const [query, setQuery] = useState('');
const [filterQuery, setFilterQuery] = useState('');
const [isPending, startTransition] = useTransition();
const filtered = users.filter(u => u.name.toLowerCase().includes(filterQuery.toLowerCase()));
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setQuery(e.target.value); // Urgent: update input immediately
startTransition(() => {
setFilterQuery(e.target.value); // Deferred: filter can wait
});
}
return (
<>
<input value={query} onChange={handleChange} />
{isPending && <Spinner />}
<UserList users={filtered} />
</>
);
}
Works for tab switching too -- wrap setTab in startTransition so heavy tab content renders without blocking other clicks.
Rule: Wrap setState in startTransition when the resulting render is expensive and the user should not be blocked.
Returns a "stale" copy of a value that lags behind during urgent updates. Defers the consumer, whereas useTransition defers the producer.
function SearchResults({ query }: { query: string }) {
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
return (
<div style={{ opacity: isStale ? 0.6 : 1 }}>
<ExpensiveList query={deferredQuery} />
</div>
);
}
Use useDeferredValue when you do not control the state update -- the value arrives as a prop. Use useTransition when you own the setState call.
function Parent() {
const [text, setText] = useState('');
return (
<>
<input value={text} onChange={e => setText(e.target.value)} />
<SlowChild text={text} />
</>
);
}
// Child defers the prop it receives
function SlowChild({ text }: { text: string }) {
const deferred = useDeferredValue(text);
return <ExpensiveTree text={deferred} />;
}
forwardRef passes a ref through a component. useImperativeHandle restricts what that ref exposes.
// Not this — parent gets the full DOM node
const FancyInput = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
return <input ref={ref} {...props} />;
});
// This — expose only intended operations
interface FancyInputHandle {
focus: () => void;
scrollIntoView: () => void;
}
const FancyInput = forwardRef<FancyInputHandle, InputProps>((props, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus() { inputRef.current?.focus(); },
scrollIntoView() { inputRef.current?.scrollIntoView({ behavior: 'smooth' }); },
}));
return <input ref={inputRef} {...props} />;
});
// Parent
function Form() {
const inputRef = useRef<FancyInputHandle>(null);
return (
<>
<FancyInput ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>Focus</button>
</>
);
}
Rule: Prefer declarative props. Use useImperativeHandle only for focus, scroll, play, measure -- actions that cannot be props.
Custom hooks compose built-in hooks to extract reusable stateful logic.
// useState + useEffect + useRef composed into a declarative interval
function useInterval(callback: () => void, delay: number | null) {
const saved = useRef(callback);
useEffect(() => { saved.current = callback; }, [callback]);
useEffect(() => {
if (delay === null) return;
const id = setInterval(() => saved.current(), delay);
return () => clearInterval(id);
}, [delay]);
}
Custom hooks compose with each other:
function useDebounce<T>(value: T, delay: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(id);
}, [value, delay]);
return debounced;
}
function useSearchResults(query: string) {
const debouncedQuery = useDebounce(query, 300);
const [results, setResults] = useState<Result[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!debouncedQuery) { setResults([]); return; }
let cancelled = false;
setLoading(true);
fetchResults(debouncedQuery).then(data => {
if (!cancelled) { setResults(data); setLoading(false); }
});
return () => { cancelled = true; };
}, [debouncedQuery]);
return { results, loading };
}
Hooks can return refs alongside state for DOM-binding patterns:
function useHover<T extends HTMLElement>() {
const ref = useRef<T>(null);
const [hovering, setHovering] = useState(false);
useEffect(() => {
const node = ref.current;
if (!node) return;
const enter = () => setHovering(true);
const leave = () => setHovering(false);
node.addEventListener('mouseenter', enter);
node.addEventListener('mouseleave', leave);
return () => { node.removeEventListener('mouseenter', enter); node.removeEventListener('mouseleave', leave); };
}, []);
return [ref, hovering] as const;
}
Rule: One hook, one job. If it needs a name like useFormValidationAndSubmissionAndAnalytics, split it.
React tracks hooks by call order in a linked list on the fiber node. Slot 0 is the first useState, slot 1 the second. Conditional calls corrupt this mapping.
// Not this — early return before hooks changes call order between renders
function Profile({ userId }: { userId: string | null }) {
if (!userId) return <p>Select a user</p>;
const [user, setUser] = useState(null); // Slot 0... sometimes
useEffect(() => { fetchUser(userId).then(setUser); }, [userId]);
return <UserCard user={user} />;
}
// This — hooks first, early return after
function Profile({ userId }: { userId: string | null }) {
const [user, setUser] = useState(null);
useEffect(() => { if (!userId) return; fetchUser(userId).then(setUser); }, [userId]);
if (!userId) return <p>Select a user</p>;
return <UserCard user={user} />;
}
No hooks in loops -- use a ref Map instead of calling useRef per item:
// Not this
const refs = tags.map(() => useRef<HTMLInputElement>(null)); // Hook count varies
// This
const refsMap = useRef<Map<string, HTMLInputElement>>(new Map());
// Use callback refs: ref={node => { if (node) refsMap.current.set(tag, node); }}
No hooks in nested functions or event handlers. Always call at the component's top level.
The invariant: Every render must call the exact same hooks in the exact same order.
Ref mirroring state -- countRef.current = count after every render is pointless duplication. Ref is for non-rendered values only.
useLayoutEffect for data fetching -- Blocks paint for a network request. Always use useEffect for async work.
Overusing useImperativeHandle -- Exposing focus, blur, getValue, setValue, validate, reset reinvents an uncontrolled component. Use controlled props.
Wrapping every setState in startTransition -- Not every update is expensive. Transitions add scheduling overhead. Measure first.
Custom hook returning 6+ values -- The hook does too much. Split useFetcher into useFetch + useFetchControl.
| Situation | Hook |
|---|---|
| Value not displayed in JSX | useRef |
| Access a DOM node | useRef + ref prop |
| DOM measurement/mutation before paint | useLayoutEffect |
| Sync with external system after paint | useEffect |
| Accessible IDs in SSR apps | useId |
| Keep UI responsive during expensive setState | useTransition |
| Defer rendering of a received prop | useDeferredValue |
| Expose imperative child API | useImperativeHandle + forwardRef |
| Reuse stateful logic | Custom hook |
| useEffect vs useLayoutEffect | useEffect | useLayoutEffect |
|---|---|---|
| Flicker without it? | No | Yes |
| Reads DOM measurements? | No | Yes |
| Involves network/timers? | Yes | No |
| useTransition vs useDeferredValue | useTransition | useDeferredValue |
|---|---|---|
| You own the setState? | Yes | No |
| Value arrives as prop? | No | Yes |
| Need isPending? | Yes | No (compare values) |
useRef for non-rendered values, useState for rendered valuesuseLayoutEffect justified by DOM measurement or flicker preventionuseId used for all htmlFor, aria-describedby, aria-labelledbystartTransition wraps only genuinely expensive updatesuseDeferredValue used when state update is in an ancestoruseImperativeHandle exposes minimal API, not the full DOM nodeuseLayoutEffect for data fetching or async work