MeshWorld India LogoMeshWorld.
CheatsheetReactJavaScriptFrontendWeb DevDeveloper Tools10 min read

React Hooks Cheat Sheet: All Hooks with React 19

Scarlett
By Scarlett
|Updated: May 20, 2026
React Hooks Cheat Sheet: All Hooks with React 19
TL;DR
  • React 19 (stable Dec 2024) new hooks: use(), useOptimistic, useFormStatus, useActionState
  • useState for values, useReducer for complex state logic, useRef for DOM and mutable values
  • useEffect for side effects — always return a cleanup function when subscribing
  • useMemo caches computed values, useCallback caches function references — don’t overuse
  • Hooks only work in function components (not class components) and cannot be called conditionally

Quick reference — all hooks at a glance

HookPurposeRe-renders?
useStateLocal state valueYes, on state change
useReducerComplex state machineYes, on dispatch
useEffectSide effects (fetch, subscribe, DOM)No
useLayoutEffectSame as useEffect but fires sync before paintNo
useRefMutable ref (DOM or any value)No
useMemoCache a computed valueNo
useCallbackCache a function referenceNo
useContextRead from a ContextYes, on context value change
useIdGenerate stable unique IDsNo
useTransitionMark state update as non-urgentNo
useDeferredValueDefer a value update (like debounce)No
useImperativeHandleExpose methods on a ref to parentNo
useDebugValueLabel custom hook in DevToolsNo
use() (React 19)Read a Promise or Context (anywhere)Suspends
useOptimistic (React 19)Show optimistic UI during async actionYes
useFormStatus (React 19)Read parent form’s submission stateYes
useActionState (React 19)Manage state driven by form actionsYes

useState

jsx
const [value, setValue] = useState(initialValue)
jsx
// Number
const [count, setCount] = useState(0)

// Object — always spread when updating
const [user, setUser] = useState({ name: 'Alice', age: 30 })
setUser(prev => ({ ...prev, age: 31 }))   // Correct
setUser({ age: 31 })                        // Wrong — loses name

// Array — always return new array
const [items, setItems] = useState([])
setItems(prev => [...prev, newItem])        // Add
setItems(prev => prev.filter(i => i.id !== id))  // Remove

// Lazy initialization (runs only once)
const [data, setData] = useState(() => expensiveComputation())
Stale Closure Trap

Always use the functional form setState(prev => ...) when the new state depends on the old state. Using setState(count + 1) inside a closure captures a stale count if called multiple times in the same render cycle.


useReducer

Use when state transitions are complex or multiple sub-values update together:

jsx
const initialState = { count: 0, step: 1 }

function reducer(state, action) {
    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 }
        case 'reset':     return initialState
        default: throw new Error('Unknown action: ' + action.type)
    }
}

function Counter() {
    const [state, dispatch] = useReducer(reducer, initialState)

    return (
        <>
            <p>{state.count}</p>
            <button onClick={() => dispatch({ type: 'increment' })}>+</button>
            <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
        </>
    )
}

useEffect

jsx
// Runs after every render
useEffect(() => { ... })

// Runs once (on mount)
useEffect(() => { ... }, [])

// Runs when dep1 or dep2 changes
useEffect(() => { ... }, [dep1, dep2])

// With cleanup (unsubscribe, clear timer, abort fetch)
useEffect(() => {
    const controller = new AbortController()
    fetch('/api/data', { signal: controller.signal })
        .then(r => r.json())
        .then(setData)

    return () => controller.abort()   // Cleanup on unmount or re-run
}, [id])
Pattern[] depsNotes
Fetch on mount[]Add cleanup to abort the request
Subscribe to event[]Return () => window.removeEventListener(...)
Re-fetch on prop change[id]Include all values read from the component
Run on every renderomit arrayRare — usually a bug
React StrictMode Runs Effects Twice

In development, StrictMode mounts → unmounts → mounts every component to catch missing cleanups. Your cleanup function must correctly undo the effect or you’ll see double-fire behavior in dev that is invisible in production.


useRef

Two uses: hold a DOM node, or hold any mutable value that does NOT trigger re-renders:

jsx
// 1 — DOM reference
function TextInput() {
    const inputRef = useRef(null)

    return (
        <>
            <input ref={inputRef} />
            <button onClick={() => inputRef.current.focus()}>Focus</button>
        </>
    )
}

// 2 — Mutable value (like an instance variable)
function Timer() {
    const intervalRef = useRef(null)

    const start = () => {
        intervalRef.current = setInterval(() => tick(), 1000)
    }

    const stop = () => {
        clearInterval(intervalRef.current)
    }

    return <><button onClick={start}>Start</button><button onClick={stop}>Stop</button></>
}

useMemo and useCallback

Both are performance tools. Only add them when you have a measured problem.

useMemo — cache a computed value

jsx
// Without useMemo: runs on every render
const sorted = items.sort((a, b) => a.price - b.price)

// With useMemo: only re-runs when items changes
const sorted = useMemo(
    () => [...items].sort((a, b) => a.price - b.price),
    [items]
)

useCallback — cache a function reference

jsx
// Without useCallback: new function reference every render
// This causes child to re-render even if nothing changed
const handleClick = () => doSomething(id)

// With useCallback: same reference until id changes
const handleClick = useCallback(
    () => doSomething(id),
    [id]
)
When to useWhen NOT to use
Passing callback to React.memo childCallbacks called once, not passed down
Expensive array/object transformationsSimple value reads
Dependency of another hookEvery function by default

useContext

jsx
// 1 — Create a context
const ThemeContext = createContext('light')

// 2 — Provide it high in the tree
function App() {
    const [theme, setTheme] = useState('light')
    return (
        <ThemeContext.Provider value={{ theme, setTheme }}>
            <Layout />
        </ThemeContext.Provider>
    )
}

// 3 — Consume it anywhere below
function Button() {
    const { theme } = useContext(ThemeContext)
    return <button className={theme}>Click</button>
}

useTransition and useDeferredValue

Both make UI feel faster by deferring non-urgent updates:

jsx
// useTransition — mark an update as non-urgent
const [isPending, startTransition] = useTransition()

function handleSearch(query) {
    // Urgent: update the input immediately
    setInputValue(query)

    // Non-urgent: update search results (won't block typing)
    startTransition(() => {
        setSearchResults(filterData(query))
    })
}

// Show a loading indicator while transition is running
{isPending && <Spinner />}
jsx
// useDeferredValue — defer a value (React controls timing)
const deferredQuery = useDeferredValue(searchQuery)

// Pass deferredQuery to expensive component instead of searchQuery
<ExpensiveList query={deferredQuery} />

React 19 hooks

use() — read Promises and Context anywhere

jsx
// Read a promise (must be wrapped in Suspense)
function UserProfile({ userPromise }) {
    const user = use(userPromise)   // Suspends until resolved
    return <h1>{user.name}</h1>
}

<Suspense fallback={<Spinner />}>
    <UserProfile userPromise={fetchUser(id)} />
</Suspense>

// Read context (same as useContext, but can be conditional)
const theme = use(ThemeContext)

useActionState — form state driven by Server Actions

jsx
async function submitForm(prevState, formData) {
    const name = formData.get('name')
    if (!name) return { error: 'Name required' }
    await saveToDatabase(name)
    return { success: true }
}

function Form() {
    const [state, formAction, isPending] = useActionState(submitForm, null)

    return (
        <form action={formAction}>
            <input name="name" />
            <button disabled={isPending}>
                {isPending ? 'Saving...' : 'Save'}
            </button>
            {state?.error && <p>{state.error}</p>}
        </form>
    )
}

useFormStatus — read parent form state

jsx
// Must be used in a component INSIDE the form
function SubmitButton() {
    const { pending, data, method } = useFormStatus()
    return <button disabled={pending}>{pending ? 'Submitting...' : 'Submit'}</button>
}

function Form() {
    return (
        <form action={serverAction}>
            <input name="email" />
            <SubmitButton />   {/* useFormStatus reads this form */}
        </form>
    )
}

useOptimistic — optimistic UI updates

jsx
function MessageList({ messages, sendMessage }) {
    const [optimisticMessages, addOptimistic] = useOptimistic(
        messages,
        (state, newMsg) => [...state, { ...newMsg, sending: true }]
    )

    async function handleSend(formData) {
        const text = formData.get('text')
        addOptimistic({ text })     // Show immediately
        await sendMessage(text)      // Then actually send
    }

    return (
        <>
            {optimisticMessages.map(m => (
                <div key={m.id} style={{ opacity: m.sending ? 0.5 : 1 }}>
                    {m.text}
                </div>
            ))}
            <form action={handleSend}>
                <input name="text" />
                <button>Send</button>
            </form>
        </>
    )
}

Custom hooks

Extract reusable logic into a function named use*:

jsx
// useLocalStorage
function useLocalStorage(key, initialValue) {
    const [value, setValue] = useState(
        () => JSON.parse(localStorage.getItem(key) ?? JSON.stringify(initialValue))
    )

    const setStored = (newValue) => {
        setValue(newValue)
        localStorage.setItem(key, JSON.stringify(newValue))
    }

    return [value, setStored]
}

// useDebounce
function useDebounce(value, delay = 300) {
    const [debounced, setDebounced] = useState(value)

    useEffect(() => {
        const t = setTimeout(() => setDebounced(value), delay)
        return () => clearTimeout(t)
    }, [value, delay])

    return debounced
}

// useFetch
function useFetch(url) {
    const [data, setData] = useState(null)
    const [loading, setLoading] = useState(true)
    const [error, setError] = useState(null)

    useEffect(() => {
        const controller = new AbortController()
        setLoading(true)
        fetch(url, { signal: controller.signal })
            .then(r => r.json())
            .then(setData)
            .catch(e => { if (e.name !== 'AbortError') setError(e) })
            .finally(() => setLoading(false))
        return () => controller.abort()
    }, [url])

    return { data, loading, error }
}

Rules of Hooks

plaintext
✅ Call hooks at the top level of a function component
✅ Call hooks at the top level of a custom hook
✅ Name custom hooks with the use prefix

❌ Call hooks inside if/else, loops, or nested functions
❌ Call hooks in class components
❌ Call hooks in regular JavaScript functions
❌ Call hooks conditionally

Summary

  • useState + useReducer = all state needs. Use reducer when you have 3+ related state values or complex transitions.
  • useEffect cleanup is mandatory for subscriptions, timers, and fetches.
  • useRef does not cause re-renders — use it for DOM access and non-reactive values.
  • useMemo / useCallback are optimization tools, not defaults — measure first.
  • React 19’s use(), useActionState, useFormStatus, useOptimistic make async and form patterns vastly simpler.

FAQ

When should I use useReducer instead of useState? When the next state depends on a complex combination of the previous state, or when you have 3+ related values that always update together. useState is fine for simple, independent values.

Why does my useEffect run twice in development? React’s StrictMode intentionally double-invokes effects to surface missing cleanup functions. It only happens in development mode. Fix it by returning a proper cleanup from the effect.

Can I call a hook from inside a condition? No. The order of hook calls must be the same on every render — React uses call order to associate state with the right hook. Conditionally render a component instead, and put the hook inside that component.

What is the difference between useMemo and useCallback? useMemo returns the result of a function (useMemo(() => compute(), [deps])). useCallback returns the function itself (useCallback(() => doThing(), [deps])). useCallback(fn, deps) is equivalent to useMemo(() => fn, deps).

Do hooks work in React Server Components? No. Server Components run on the server and don’t have state or lifecycle. Hooks only work in Client Components (files with "use client" or in classic React without the framework).


Share_This Twitter / X
Scarlett
Written By

Scarlett

Frontend Engineer and Design Systems specialist. Focused on building type-safe UI architectures, scalable CSS patterns, and accessible user experiences that bridge the gap between design and logic.

Enjoyed this article?

Support MeshWorld and help us create more technical content