
React Syntax Hooks Basic
- 11 installs
- 6 repo stars
- Updated July 8, 2026
- openaec-foundation/react-claude-skill-package
Helps with frontend development tasks.
About
react-syntax-hooks-basic is a Claude Code skill for frontend development. It helps solo builders move faster with AI-assisted development.
- react-syntax-hooks-basic
- Frontend Development
- AI-coding skill
React Syntax Hooks Basic 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-hooks-basicAdd 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-hooks-basic
Quick Reference
The 7 Essential Hooks
| Hook | Purpose | Returns |
|---|---|---|
useState<S> | Local component state | [state, setState] |
useEffect | Side effects after render | void |
useContext<T> | Consume context value | T |
useRef<T> | Mutable ref / DOM access | { current: T } |
useMemo<T> | Memoize expensive values | T |
useCallback<T> | Memoize function references | T |
useReducer<S, A> | Complex state with actions | [state, dispatch] |
Import
import { useState, useEffect, useContext, useRef, useMemo, useCallback, useReducer } from 'react';Critical Warnings: Rules of Hooks
NEVER call hooks inside loops, conditions, nested functions, try/catch/finally blocks, or after conditional return statements. React relies on consistent call order to maintain state correctly.
NEVER call hooks from regular JavaScript functions. ALWAYS call hooks from React function components or custom hooks (functions starting with use).
ALWAYS call hooks at the top level of your function component or custom hook — before any early returns.
ALWAYS use eslint-plugin-react-hooks to catch violations automatically.
// WRONG — conditional hook call
function Profile({ userId }: { userId: string | null }) {
if (!userId) return null;
const [user, setUser] = useState<User | null>(null); // VIOLATION
}
// CORRECT — hooks before early return
function Profile({ userId }: { userId: string | null }) {
const [user, setUser] = useState<User | null>(null);
if (!userId) return null;
// ... rest of component
}---
useState: Local State
Signature
const [state, setState] = useState<S>(initialState: S | (() => S));Decision Tree
- Need state that triggers re-render on change? →
useState - Initial value is expensive to compute? → Pass a function:
useState(() => computeExpensive()) - State depends on previous state? → Use updater function:
setState(prev => prev + 1) - State is an object or array? → ALWAYS create a new reference when updating
Key Rules
setStateis stable across renders — safe to omit from dependency arrays.- State updates are batched — the screen updates after all event handlers complete.
Object.is()comparison — identical values skip re-render.- Calling
setStatedoes NOT update the variable in the currently executing code.
TypeScript Typing
const [count, setCount] = useState<number>(0);
const [user, setUser] = useState<User | null>(null);
const [items, setItems] = useState<string[]>([]);React 18 vs 19: No API changes. Behavior consistent across versions.
---
useEffect: Side Effects
Signature
useEffect(setup: () => (void | (() => void)), dependencies?: unknown[]): void;Dependency Array Behavior
| Pattern | Runs when | Use case |
|---|---|---|
[dep1, dep2] | Any dependency changes | Most common — sync to specific values |
[] | Only on mount (cleanup on unmount) | One-time setup (subscriptions, timers) |
| Omitted | Every commit | Rarely needed — usually a mistake |
Decision Tree
- Subscribing to external system? →
useEffectwith cleanup - Fetching data? →
useEffectwith ignore flag for race conditions - Need effect before paint? → Use
useLayoutEffectinstead - Transforming data for render? → Do it during render, NOT in an effect
Key Rules
NEVER pass an async function directly as the effect callback. Effects must return void or a cleanup function — async returns a Promise.
// WRONG
useEffect(async () => {
const data = await fetchData();
}, []);
// CORRECT
useEffect(() => {
let ignore = false;
async function fetchData() {
const result = await api.getData(id);
if (!ignore) setData(result);
}
fetchData();
return () => { ignore = true; };
}, [id]);ALWAYS return a cleanup function when your effect creates subscriptions, timers, or event listeners.
useEffect(() => {
const handler = (e: KeyboardEvent) => { /* ... */ };
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, []);- Runs AFTER browser paint (non-blocking).
- Does NOT run during SSR.
- StrictMode (dev): runs setup → cleanup → setup to catch missing cleanups.
React 18 vs 19: No API changes.
---
useContext: Context Consumption
Signature
const value = useContext<T>(context: React.Context<T>): T;Key Rules
- Returns the value from the closest matching
<Provider>above in the tree. - Falls back to
defaultValuefromcreateContextif no provider found. - A provider in the same component does NOT affect
useContextin that component. - React re-renders ALL consumers when the provider value changes (
Object.iscomparison). memo()does NOT prevent receiving fresh context values.
TypeScript Typing
interface ThemeContextType {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | null>(null);
function useTheme(): ThemeContextType {
const ctx = useContext(ThemeContext);
if (ctx === null) throw new Error('useTheme must be used within ThemeProvider');
return ctx;
}React 18 vs 19: In React 19, use <Context value={...}> directly. In React 18, use <Context.Provider value={...}>.
---
useRef: Refs and Mutable Values
Signature
const ref = useRef<T>(initialValue: T): { current: T };Decision Tree
- Need a DOM element reference? →
useRef<HTMLElement>(null) - Need a mutable value that does NOT trigger re-render? →
useRef - Need to store a timeout/interval ID? →
useRef - Need a value that triggers re-render on change? → Use
useStateinstead
Key Rules
- Changing
.currentdoes NOT trigger a re-render. - NEVER read or write
.currentduring render (breaks component purity). Use it in event handlers and effects only. - Same object identity across renders.
TypeScript Typing
const inputRef = useRef<HTMLInputElement>(null);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const renderCount = useRef<number>(0);ALWAYS type DOM refs as HTMLElement | null with initial value null:
const divRef = useRef<HTMLDivElement>(null);React 18 vs 19: No API changes.
---
useMemo: Memoize Values
Signature
const memoized = useMemo<T>(factory: () => T, deps: unknown[]): T;Decision Tree
- Calculation is noticeably slow AND dependencies rarely change? →
useMemo - Value is passed to a
memo()-wrapped child? →useMemo - Value is used as a dependency of another hook? →
useMemo - Calculation is trivial? → Do NOT use
useMemo— the overhead outweighs the benefit
Key Rules
- Factory must be a pure function with no arguments.
- Dependencies compared via
Object.is. - React MAY discard the cache — do NOT rely on it as a semantic guarantee.
- React Compiler (React 19) auto-memoizes, reducing need for manual
useMemo.
React 18 vs 19: React Compiler in 19 makes manual useMemo optional in many cases.
---
useCallback: Memoize Functions
Signature
const memoized = useCallback<T extends (...args: any[]) => any>(fn: T, deps: unknown[]): T;Decision Tree
- Passing a function to a
memo()-wrapped child? →useCallback - Function is a dependency of
useEffector another hook? →useCallback - Function is only used in event handlers, not passed down? → Do NOT use
useCallback
Equivalence with useMemo
useCallback(fn, deps) === useMemo(() => fn, deps)Updater Pattern: Remove State Dependencies
// WRONG — deps change every update
const handleAdd = useCallback((text: string) => {
setTodos([...todos, createTodo(text)]);
}, [todos]);
// CORRECT — empty deps, stable reference
const handleAdd = useCallback((text: string) => {
setTodos(prev => [...prev, createTodo(text)]);
}, []);React 18 vs 19: React Compiler in 19 makes manual useCallback optional in many cases.
---
useReducer: Complex State
Signature
const [state, dispatch] = useReducer<S, A>(
reducer: (state: S, action: A) => S,
initialArg: S,
init?: (arg: S) => S
): [S, Dispatch<A>];Decision Tree
- State has multiple sub-values updated together? →
useReducer - State transitions depend on complex logic? →
useReducer - Need to pass state update logic down without prop drilling? →
useReducer+ context - Simple boolean or single value? →
useStateis simpler
Key Rules
- Reducer MUST be pure — NEVER mutate state, ALWAYS return a new object.
dispatchis stable — safe to omit from dependency arrays.- Convention: actions are objects with a
typeproperty. - Lazy init: pass
initfunction as third argument to avoid recreating initial state.
TypeScript Typing
type State = { count: number; step: number };
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'setStep'; payload: number };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment': return { ...state, count: state.count + state.step };
case 'decrement': return { ...state, count: state.count - state.step };
case 'setStep': return { ...state, step: action.payload };
}
}
const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 });React 18 vs 19: No API changes.
---
Dependency Array Rules
Referential Equality
Dependencies are compared with Object.is. For primitives this is value comparison. For objects, arrays, and functions it is reference comparison.
| Dependency type | Stable across renders? | Solution if unstable |
|---|---|---|
| Primitive (string, number, boolean) | Yes (same value = same) | No action needed |
| Object/array created in render | No (new reference each render) | useMemo or move outside |
| Function created in render | No (new reference each render) | useCallback or move outside |
State setter (setState) | Yes (stable identity) | Safe to omit |
dispatch from useReducer | Yes (stable identity) | Safe to omit |
Ref object from useRef | Yes (stable identity) | Safe to omit |
Common Mistakes
NEVER pass an object or array literal as a dependency — it creates a new reference every render, causing the effect to run on every render:
// WRONG — runs every render
useEffect(() => { /* ... */ }, [{ id: userId }]);
// CORRECT — use the primitive value
useEffect(() => { /* ... */ }, [userId]);NEVER omit dependencies to "fix" infinite loops — fix the root cause (unstable references) instead.
---
Reference Links
- references/examples.md — Working code examples for all 7 hooks
- references/api-table.md — Complete hook signatures and return types
- references/anti-patterns.md — What NOT to do, with explanations
Official Sources
- https://react.dev/reference/react/useState
- https://react.dev/reference/react/useEffect
- https://react.dev/reference/react/useContext
- https://react.dev/reference/react/useRef
- https://react.dev/reference/react/useMemo
- https://react.dev/reference/react/useCallback
- https://react.dev/reference/react/useReducer
- https://react.dev/reference/rules/rules-of-hooks
Hook Anti-Patterns
What NOT to do when using React hooks, with explanations and correct alternatives.
Source: https://react.dev/reference/react/
---
Rules of Hooks Violations
Conditional Hook Calls
// WRONG — hooks called conditionally
function UserProfile({ userId }: { userId: string | null }) {
if (!userId) {
return <p>No user selected</p>;
}
// VIOLATION: hook call order changes when userId is null
const [user, setUser] = useState<User | null>(null);
useEffect(() => { fetchUser(userId).then(setUser); }, [userId]);
return <div>{user?.name}</div>;
}
// CORRECT — hooks ALWAYS at top level
function UserProfile({ userId }: { userId: string | null }) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
if (!userId) return;
let ignore = false;
fetchUser(userId).then(u => { if (!ignore) setUser(u); });
return () => { ignore = true; };
}, [userId]);
if (!userId) return <p>No user selected</p>;
return <div>{user?.name}</div>;
}Hooks Inside Loops
// WRONG — hook called in loop
function MultiCounter({ count }: { count: number }) {
const values: number[] = [];
for (let i = 0; i < count; i++) {
const [val] = useState(0); // VIOLATION
values.push(val);
}
return <div>{values.join(', ')}</div>;
}
// CORRECT — extract to child component
function Counter() {
const [val, setVal] = useState<number>(0);
return <button onClick={() => setVal(v => v + 1)}>{val}</button>;
}
function MultiCounter({ count }: { count: number }) {
return (
<div>
{Array.from({ length: count }, (_, i) => <Counter key={i} />)}
</div>
);
}Hooks in Event Handlers
// WRONG — hook called inside event handler
function SearchBox() {
const handleSearch = () => {
const [query, setQuery] = useState(''); // VIOLATION
};
return <button onClick={handleSearch}>Search</button>;
}
// CORRECT — hook at top level
function SearchBox() {
const [query, setQuery] = useState<string>('');
const handleSearch = () => {
// use query here
console.log('Searching:', query);
};
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<button onClick={handleSearch}>Search</button>
</div>
);
}---
useState Anti-Patterns
Mutating State Directly
// WRONG — mutating existing object
const [user, setUser] = useState<{ name: string; age: number }>({ name: 'Alice', age: 30 });
const updateAge = () => {
user.age = 31; // Mutation — React does not detect this
setUser(user); // Same reference — React skips re-render
};
// CORRECT — create new object
const updateAge = () => {
setUser(prev => ({ ...prev, age: 31 }));
};Mutating Array State
// WRONG — push mutates the existing array
const [items, setItems] = useState<string[]>([]);
const addItem = (item: string) => {
items.push(item); // Mutation
setItems(items); // Same reference — no re-render
};
// CORRECT — spread into new array
const addItem = (item: string) => {
setItems(prev => [...prev, item]);
};Calling Initializer Function
// WRONG — createRows() called on EVERY render
const [rows, setRows] = useState(createRows());
// CORRECT — pass function reference, called once on mount
const [rows, setRows] = useState(createRows);
// or: useState(() => createRows())Reading State Right After Setting
// WRONG — expects immediate update
function Counter() {
const [count, setCount] = useState<number>(0);
const handleClick = () => {
setCount(count + 1);
console.log(count); // Still 0! State updates are batched.
};
}
// CORRECT — use updater for sequential updates, or accept the batched behavior
const handleClick = () => {
setCount(prev => {
const next = prev + 1;
console.log('Next:', next);
return next;
});
};---
useEffect Anti-Patterns
Async Callback
// WRONG — async function returns Promise, not cleanup function
useEffect(async () => {
const data = await fetchData();
setData(data);
}, []);
// CORRECT — define async function inside effect
useEffect(() => {
let ignore = false;
async function load() {
const data = await fetchData();
if (!ignore) setData(data);
}
load();
return () => { ignore = true; };
}, []);Missing Cleanup
// WRONG — event listener leaks on unmount and re-render
useEffect(() => {
window.addEventListener('scroll', handleScroll);
}, []);
// CORRECT — cleanup removes listener
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);Object in Dependency Array
// WRONG — new object reference every render, effect runs every render
function Chart({ data }: { data: number[] }) {
const options = { animate: true, color: 'blue' };
useEffect(() => {
drawChart(data, options);
}, [data, options]); // options is a new object every render!
}
// CORRECT — move object inside effect or memoize
function Chart({ data }: { data: number[] }) {
useEffect(() => {
const options = { animate: true, color: 'blue' };
drawChart(data, options);
}, [data]);
}
// OR: extract primitives as dependencies
function Chart({ data, animate, color }: { data: number[]; animate: boolean; color: string }) {
useEffect(() => {
drawChart(data, { animate, color });
}, [data, animate, color]);
}Missing Race Condition Guard
// WRONG — stale response overwrites fresh data
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
// If userId changes rapidly: response for old userId may arrive AFTER new one
// CORRECT — ignore stale responses
useEffect(() => {
let ignore = false;
fetchUser(userId).then(user => {
if (!ignore) setUser(user);
});
return () => { ignore = true; };
}, [userId]);Omitting Dependencies to Prevent Infinite Loops
// WRONG — suppressing the linter instead of fixing the issue
useEffect(() => {
fetchData(options); // options changes every render
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // Missing dependency: options
// CORRECT — stabilize the dependency
const stableOptions = useMemo(() => options, [options.key1, options.key2]);
useEffect(() => {
fetchData(stableOptions);
}, [stableOptions]);---
useContext Anti-Patterns
Missing Provider Check
// WRONG — returns undefined silently if no provider
function useTheme() {
return useContext(ThemeContext); // Could be undefined
}
// CORRECT — explicit null check with helpful error
function useTheme(): Theme {
const ctx = useContext(ThemeContext);
if (ctx === null) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return ctx;
}Creating New Object Every Render in Provider
// WRONG — all consumers re-render on every parent render
function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
// { theme, setTheme } is a NEW object every render
}
// CORRECT — memoize the context value
function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const value = useMemo(() => ({ theme, setTheme }), [theme]);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}---
useRef Anti-Patterns
Reading/Writing ref.current During Render
// WRONG — impure render, breaks concurrent features
function Counter() {
const countRef = useRef<number>(0);
countRef.current += 1; // Writing during render — VIOLATION
return <p>Renders: {countRef.current}</p>; // Reading during render — VIOLATION
}
// CORRECT — read/write in effects or event handlers
function Counter() {
const countRef = useRef<number>(0);
useEffect(() => {
countRef.current += 1;
});
return <p>Check console for render count</p>;
}Using ref When State is Needed
// WRONG — changing ref does not trigger re-render
function NameDisplay() {
const nameRef = useRef<string>('');
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
nameRef.current = e.target.value; // UI does NOT update
};
return (
<div>
<input onChange={handleChange} />
<p>Name: {nameRef.current}</p> {/* Always shows initial value */}
</div>
);
}
// CORRECT — use state for values that affect rendering
function NameDisplay() {
const [name, setName] = useState<string>('');
return (
<div>
<input value={name} onChange={(e) => setName(e.target.value)} />
<p>Name: {name}</p>
</div>
);
}---
useMemo / useCallback Anti-Patterns
Premature Memoization
// WRONG — memoizing a trivial calculation adds overhead
const doubled = useMemo(() => count * 2, [count]);
// CORRECT — just compute it
const doubled = count * 2;Missing useMemo When Passing Objects to memo() Children
// WRONG — memo() on child is useless because style is new object every render
const ExpensiveChild = memo(function ExpensiveChild({ style }: { style: React.CSSProperties }) {
return <div style={style}>Expensive</div>;
});
function Parent() {
return <ExpensiveChild style={{ color: 'red', fontSize: 16 }} />;
// style is a NEW object every render — memo() never skips
}
// CORRECT — memoize the object
function Parent() {
const style = useMemo<React.CSSProperties>(() => ({ color: 'red', fontSize: 16 }), []);
return <ExpensiveChild style={style} />;
}useCallback Without memo() Child
// WRONG — useCallback alone does nothing useful
function Parent() {
const [count, setCount] = useState<number>(0);
// This memoization has no effect if Child is NOT wrapped in memo()
const handleClick = useCallback(() => {
setCount(prev => prev + 1);
}, []);
return <Child onClick={handleClick} />;
}
// CORRECT — useCallback + memo() together
const Child = memo(function Child({ onClick }: { onClick: () => void }) {
return <button onClick={onClick}>Click</button>;
});---
useReducer Anti-Patterns
Mutating State in Reducer
// WRONG — mutates state object
function reducer(state: { items: string[] }, action: { type: 'add'; item: string }) {
state.items.push(action.item); // Mutation!
return state; // Same reference — React may skip re-render
}
// CORRECT — return new state
function reducer(state: { items: string[] }, action: { type: 'add'; item: string }) {
return { items: [...state.items, action.item] };
}Calling Lazy Initializer Instead of Passing It
// WRONG — createInitialState() runs every render
const [state, dispatch] = useReducer(reducer, createInitialState());
// CORRECT — pass as third argument, called once with second argument
const [state, dispatch] = useReducer(reducer, defaultArg, createInitialState);---
Dependency Array Anti-Patterns Summary
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Object literal in deps [{ id }] | New reference every render | Use primitive: [id] |
Array literal in deps [[a, b]] | New reference every render | Use elements: [a, b] |
Function in deps without useCallback | New reference every render | Wrap in useCallback |
| Omitting deps to "fix" loops | Stale closures, bugs | Stabilize the values instead |
// eslint-disable on deps rule | Hides real bugs | Fix the dependency, do not suppress |
Empty [] when deps exist | Effect uses stale values | Include all reactive values |
Hook Signatures and Return Types
Complete TypeScript signatures for the 7 essential React hooks.
Source: https://react.dev/reference/react/
---
useState
function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];
type SetStateAction<S> = S | ((prevState: S) => S);
type Dispatch<A> = (value: A) => void;| Parameter | Type | Description |
|---|---|---|
initialState | `S \ | (() => S)` |
| Return | Type | Description |
|---|---|---|
[0] state | S | Current state value |
[1] setState | Dispatch<SetStateAction<S>> | Setter — accepts value or updater function. Stable identity. |
setState Behavior
| Call pattern | Effect |
|---|---|
setState(newValue) | Replaces state with newValue |
setState(prev => next) | Computes next state from previous |
setState(sameValue) | Skips re-render (Object.is comparison) |
---
useEffect
function useEffect(
setup: () => (void | (() => void)),
dependencies?: ReadonlyArray<unknown>
): void;| Parameter | Type | Description |
|---|---|---|
setup | `() => (void \ | (() => void))` |
dependencies | `ReadonlyArray<unknown> \ | undefined` |
| Return | Type | Description |
|---|---|---|
| — | void | No return value |
Execution Timing
| Event | What happens |
|---|---|
| Mount | setup() runs after browser paint |
| Dependency change | cleanup() with old values → setup() with new values |
| Unmount | cleanup() runs |
| StrictMode (dev) | setup() → cleanup() → setup() on mount |
---
useContext
function useContext<T>(context: React.Context<T>): T;| Parameter | Type | Description |
|---|---|---|
context | React.Context<T> | Context object from createContext |
| Return | Type | Description |
|---|---|---|
| value | T | Value from closest Provider above, or defaultValue |
createContext (companion API)
function createContext<T>(defaultValue: T): React.Context<T>;Provider Syntax
| Version | Syntax |
|---|---|
| React 18 | <MyContext.Provider value={val}> |
| React 19 | <MyContext value={val}> |
---
useRef
function useRef<T>(initialValue: T): MutableRefObject<T>;
function useRef<T>(initialValue: T | null): RefObject<T>;
function useRef<T = undefined>(): MutableRefObject<T | undefined>;
interface MutableRefObject<T> { current: T; }
interface RefObject<T> { readonly current: T | null; }| Parameter | Type | Description |
|---|---|---|
initialValue | T | Initial .current value (ignored after first render) |
| Return | Type | Description |
|---|---|---|
| ref | { current: T } | Mutable object with stable identity across renders |
TypeScript Overload Rules
| Call | Return type | .current |
|---|---|---|
useRef<HTMLDivElement>(null) | RefObject<HTMLDivElement> | readonly, `T \ |
useRef<number>(0) | MutableRefObject<number> | Mutable, T |
| `useRef<number \ | null>(null)` | `MutableRefObject<number \ |
---
useMemo
function useMemo<T>(factory: () => T, deps: ReadonlyArray<unknown>): T;| Parameter | Type | Description |
|---|---|---|
factory | () => T | Pure function, no arguments. Called on mount and when deps change. |
deps | ReadonlyArray<unknown> | Reactive values compared via Object.is |
| Return | Type | Description |
|---|---|---|
| value | T | Cached result of factory() |
Cache Guarantee
React MAY discard cached values (during development edits, Suspense, etc.). NEVER rely on useMemo for semantic correctness — it is a performance optimization only.
---
useCallback
function useCallback<T extends (...args: any[]) => any>(
callback: T,
deps: ReadonlyArray<unknown>
): T;| Parameter | Type | Description |
|---|---|---|
callback | T | Function to cache (React returns it, does NOT call it) |
deps | ReadonlyArray<unknown> | Reactive values compared via Object.is |
| Return | Type | Description |
|---|---|---|
| fn | T | Cached function reference. New function only when deps change. |
Equivalence
useCallback(fn, deps) === useMemo(() => fn, deps)---
useReducer
function useReducer<S, A>(
reducer: (state: S, action: A) => S,
initialArg: S,
init?: (arg: S) => S
): [S, Dispatch<A>];
type Dispatch<A> = (action: A) => void;| Parameter | Type | Description |
|---|---|---|
reducer | (state: S, action: A) => S | Pure function. MUST return new state, NEVER mutate. |
initialArg | S | Value from which initial state is calculated |
init | `((arg: S) => S) \ | undefined` |
| Return | Type | Description |
|---|---|---|
[0] state | S | Current state value |
[1] dispatch | Dispatch<A> | Trigger reducer. Stable identity. Returns void. |
dispatch Behavior
| Call | Effect |
|---|---|
dispatch(action) | Queues re-render with reducer(currentState, action) |
dispatch(action) when result is same | Skips re-render (Object.is comparison) |
---
Stable Identities Summary
These values are guaranteed stable across renders and safe to omit from dependency arrays:
| Value | Source |
|---|---|
setState | useState |
dispatch | useReducer |
ref object | useRef |
ALWAYS include all other reactive values in dependency arrays.
Hook Usage Examples
All examples use TypeScript with strict typing. Verified against https://react.dev/reference/react/
---
useState Examples
Basic State with Types
import { useState } from 'react';
interface User {
id: string;
name: string;
email: string;
}
function UserProfile() {
const [name, setName] = useState<string>('');
const [age, setAge] = useState<number>(0);
const [isActive, setIsActive] = useState<boolean>(false);
const [user, setUser] = useState<User | null>(null);
const [tags, setTags] = useState<string[]>([]);
return (
<div>
<input value={name} onChange={(e) => setName(e.target.value)} />
<button onClick={() => setAge(prev => prev + 1)}>Age: {age}</button>
<button onClick={() => setIsActive(prev => !prev)}>
{isActive ? 'Active' : 'Inactive'}
</button>
</div>
);
}Lazy Initialization
function ExpensiveInitialState() {
// CORRECT — function reference, called once on mount
const [data, setData] = useState<Map<string, number>>(() => {
const map = new Map<string, number>();
for (let i = 0; i < 10000; i++) {
map.set(`key-${i}`, i);
}
return map;
});
return <div>Items: {data.size}</div>;
}Object State — Immutable Updates
interface FormState {
firstName: string;
lastName: string;
email: string;
}
function RegistrationForm() {
const [form, setForm] = useState<FormState>({
firstName: '',
lastName: '',
email: '',
});
// CORRECT — spread operator creates new object
const updateField = (field: keyof FormState, value: string) => {
setForm(prev => ({ ...prev, [field]: value }));
};
return (
<form>
<input
value={form.firstName}
onChange={(e) => updateField('firstName', e.target.value)}
/>
<input
value={form.lastName}
onChange={(e) => updateField('lastName', e.target.value)}
/>
<input
value={form.email}
onChange={(e) => updateField('email', e.target.value)}
/>
</form>
);
}Array State — Immutable Updates
interface Todo {
id: number;
text: string;
done: boolean;
}
function TodoList() {
const [todos, setTodos] = useState<Todo[]>([]);
const [nextId, setNextId] = useState<number>(1);
const addTodo = (text: string) => {
setTodos(prev => [...prev, { id: nextId, text, done: false }]);
setNextId(prev => prev + 1);
};
const toggleTodo = (id: number) => {
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
)
);
};
const removeTodo = (id: number) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
};
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<span
style={{ textDecoration: todo.done ? 'line-through' : 'none' }}
onClick={() => toggleTodo(todo.id)}
>
{todo.text}
</span>
<button onClick={() => removeTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
);
}Storing a Function in State
// CORRECT — wrap in arrow function so React does not call it as initializer
const [formatter, setFormatter] = useState<(v: number) => string>(
() => (v: number) => v.toFixed(2)
);
// Update: also wrap in arrow function
setFormatter(() => (v: number) => v.toLocaleString());---
useEffect Examples
Subscription with Cleanup
import { useState, useEffect } from 'react';
function WindowSize() {
const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight });
useEffect(() => {
const handleResize = () => {
setSize({ width: window.innerWidth, height: window.innerHeight });
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []); // Empty deps — subscribe once on mount
return <div>{size.width} x {size.height}</div>;
}Data Fetching with Race Condition Guard
interface Article {
id: string;
title: string;
body: string;
}
function ArticleView({ articleId }: { articleId: string }) {
const [article, setArticle] = useState<Article | null>(null);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
let ignore = false;
setLoading(true);
async function loadArticle() {
const response = await fetch(`/api/articles/${articleId}`);
const data: Article = await response.json();
if (!ignore) {
setArticle(data);
setLoading(false);
}
}
loadArticle();
return () => { ignore = true; };
}, [articleId]);
if (loading) return <div>Loading...</div>;
if (!article) return <div>Not found</div>;
return <article><h1>{article.title}</h1><p>{article.body}</p></article>;
}Timer with Cleanup
function Stopwatch() {
const [seconds, setSeconds] = useState<number>(0);
const [isRunning, setIsRunning] = useState<boolean>(false);
useEffect(() => {
if (!isRunning) return;
const intervalId = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
return () => clearInterval(intervalId);
}, [isRunning]);
return (
<div>
<p>{seconds}s</p>
<button onClick={() => setIsRunning(prev => !prev)}>
{isRunning ? 'Stop' : 'Start'}
</button>
<button onClick={() => { setIsRunning(false); setSeconds(0); }}>Reset</button>
</div>
);
}Document Title Sync
function PageTitle({ title }: { title: string }) {
useEffect(() => {
const previousTitle = document.title;
document.title = title;
return () => { document.title = previousTitle; };
}, [title]);
return null;
}---
useContext Examples
Creating and Consuming Context
import { createContext, useContext, useState, useMemo, type ReactNode } from 'react';
interface AuthContextType {
user: { name: string; role: string } | null;
login: (name: string, role: string) => void;
logout: () => void;
}
const AuthContext = createContext<AuthContextType | null>(null);
// Custom hook with null check — ALWAYS use this pattern
function useAuth(): AuthContextType {
const context = useContext(AuthContext);
if (context === null) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<{ name: string; role: string } | null>(null);
// Memoize to prevent unnecessary consumer re-renders
const value = useMemo<AuthContextType>(() => ({
user,
login: (name: string, role: string) => setUser({ name, role }),
logout: () => setUser(null),
}), [user]);
// React 19: <AuthContext value={value}>
// React 18: <AuthContext.Provider value={value}>
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
function UserGreeting() {
const { user, logout } = useAuth();
if (!user) return <p>Please log in</p>;
return (
<div>
<p>Hello, {user.name} ({user.role})</p>
<button onClick={logout}>Logout</button>
</div>
);
}---
useRef Examples
DOM Reference
import { useRef } from 'react';
function AutoFocusInput() {
const inputRef = useRef<HTMLInputElement>(null);
const handleClick = () => {
inputRef.current?.focus();
inputRef.current?.select();
};
return (
<div>
<input ref={inputRef} type="text" placeholder="Click button to focus" />
<button onClick={handleClick}>Focus Input</button>
</div>
);
}Mutable Value (Previous State)
import { useState, useRef, useEffect } from 'react';
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>(undefined);
useEffect(() => {
ref.current = value;
});
return ref.current;
}
function Counter() {
const [count, setCount] = useState<number>(0);
const prevCount = usePrevious(count);
return (
<div>
<p>Current: {count}, Previous: {prevCount ?? 'N/A'}</p>
<button onClick={() => setCount(prev => prev + 1)}>Increment</button>
</div>
);
}Storing Interval ID
function Ticker() {
const [count, setCount] = useState<number>(0);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const start = () => {
if (intervalRef.current !== null) return; // Prevent duplicates
intervalRef.current = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
};
const stop = () => {
if (intervalRef.current !== null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
useEffect(() => {
return () => stop(); // Cleanup on unmount
}, []);
return (
<div>
<p>{count}</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}---
useMemo Examples
Expensive Computation
import { useState, useMemo } from 'react';
interface Product {
id: number;
name: string;
category: string;
price: number;
}
function ProductList({ products }: { products: Product[] }) {
const [filter, setFilter] = useState<string>('');
const [sortBy, setSortBy] = useState<'name' | 'price'>('name');
const filteredAndSorted = useMemo<Product[]>(() => {
const filtered = products.filter(p =>
p.name.toLowerCase().includes(filter.toLowerCase())
);
return filtered.sort((a, b) => {
if (sortBy === 'name') return a.name.localeCompare(b.name);
return a.price - b.price;
});
}, [products, filter, sortBy]);
return (
<div>
<input value={filter} onChange={(e) => setFilter(e.target.value)} />
<select value={sortBy} onChange={(e) => setSortBy(e.target.value as 'name' | 'price')}>
<option value="name">Name</option>
<option value="price">Price</option>
</select>
<ul>
{filteredAndSorted.map(p => (
<li key={p.id}>{p.name} — ${p.price}</li>
))}
</ul>
</div>
);
}Stable Object for Context Provider
function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const value = useMemo(() => ({
theme,
toggle: () => setTheme(prev => prev === 'light' ? 'dark' : 'light'),
}), [theme]);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}---
useCallback Examples
Stable Callback for Memo-wrapped Child
import { useState, useCallback, memo } from 'react';
interface ButtonProps {
onClick: () => void;
label: string;
}
const ExpensiveButton = memo(function ExpensiveButton({ onClick, label }: ButtonProps) {
console.log(`Rendering: ${label}`);
return <button onClick={onClick}>{label}</button>;
});
function Parent() {
const [count, setCount] = useState<number>(0);
const [name, setName] = useState<string>('');
// Stable reference — ExpensiveButton skips re-render when name changes
const increment = useCallback(() => {
setCount(prev => prev + 1);
}, []);
return (
<div>
<p>Count: {count}</p>
<input value={name} onChange={(e) => setName(e.target.value)} />
<ExpensiveButton onClick={increment} label="Increment" />
</div>
);
}Callback as Effect Dependency
function SearchResults({ query }: { query: string }) {
const [results, setResults] = useState<string[]>([]);
const fetchResults = useCallback(async () => {
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
const data: string[] = await response.json();
return data;
}, [query]);
useEffect(() => {
let ignore = false;
fetchResults().then(data => {
if (!ignore) setResults(data);
});
return () => { ignore = true; };
}, [fetchResults]);
return <ul>{results.map((r, i) => <li key={i}>{r}</li>)}</ul>;
}---
useReducer Examples
Typed Reducer with Discriminated Union Actions
import { useReducer } from 'react';
interface TodoState {
todos: Array<{ id: number; text: string; done: boolean }>;
nextId: number;
}
type TodoAction =
| { type: 'added'; text: string }
| { type: 'toggled'; id: number }
| { type: 'deleted'; id: number };
function todoReducer(state: TodoState, action: TodoAction): TodoState {
switch (action.type) {
case 'added':
return {
...state,
todos: [...state.todos, { id: state.nextId, text: action.text, done: false }],
nextId: state.nextId + 1,
};
case 'toggled':
return {
...state,
todos: state.todos.map(t =>
t.id === action.id ? { ...t, done: !t.done } : t
),
};
case 'deleted':
return {
...state,
todos: state.todos.filter(t => t.id !== action.id),
};
}
}
const initialState: TodoState = { todos: [], nextId: 1 };
function TodoApp() {
const [state, dispatch] = useReducer(todoReducer, initialState);
return (
<div>
<button onClick={() => dispatch({ type: 'added', text: 'New task' })}>
Add Todo
</button>
<ul>
{state.todos.map(todo => (
<li key={todo.id}>
<span
onClick={() => dispatch({ type: 'toggled', id: todo.id })}
style={{ textDecoration: todo.done ? 'line-through' : 'none' }}
>
{todo.text}
</span>
<button onClick={() => dispatch({ type: 'deleted', id: todo.id })}>
Delete
</button>
</li>
))}
</ul>
</div>
);
}Lazy Initialization
interface AppState {
settings: Record<string, string>;
}
function createInitialState(defaultSettings: Record<string, string>): AppState {
// Expensive: reads from localStorage, merges with defaults
const stored = JSON.parse(localStorage.getItem('settings') ?? '{}') as Record<string, string>;
return { settings: { ...defaultSettings, ...stored } };
}
type AppAction = { type: 'update'; key: string; value: string };
function settingsReducer(state: AppState, action: AppAction): AppState {
switch (action.type) {
case 'update':
return { settings: { ...state.settings, [action.key]: action.value } };
}
}
function Settings() {
// Third argument: lazy init function, called once with second argument
const [state, dispatch] = useReducer(
settingsReducer,
{ theme: 'light', lang: 'en' },
createInitialState
);
return <div>{JSON.stringify(state.settings)}</div>;
}useReducer + useContext for State Management
import { createContext, useContext, useReducer, type ReactNode, type Dispatch } from 'react';
// Define state and actions
interface CountState { count: number }
type CountAction = { type: 'increment' } | { type: 'decrement' } | { type: 'reset' };
function countReducer(state: CountState, action: CountAction): CountState {
switch (action.type) {
case 'increment': return { count: state.count + 1 };
case 'decrement': return { count: state.count - 1 };
case 'reset': return { count: 0 };
}
}
// Two contexts: one for state, one for dispatch (dispatch is stable, avoids unnecessary re-renders)
const CountStateContext = createContext<CountState | null>(null);
const CountDispatchContext = createContext<Dispatch<CountAction> | null>(null);
function CountProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(countReducer, { count: 0 });
return (
<CountStateContext.Provider value={state}>
<CountDispatchContext.Provider value={dispatch}>
{children}
</CountDispatchContext.Provider>
</CountStateContext.Provider>
);
}
function useCountState(): CountState {
const ctx = useContext(CountStateContext);
if (ctx === null) throw new Error('useCountState must be within CountProvider');
return ctx;
}
function useCountDispatch(): Dispatch<CountAction> {
const ctx = useContext(CountDispatchContext);
if (ctx === null) throw new Error('useCountDispatch must be within CountProvider');
return ctx;
}