- React 19 (stable Dec 2024) new hooks:
use(),useOptimistic,useFormStatus,useActionState useStatefor values,useReducerfor complex state logic,useReffor DOM and mutable valuesuseEffectfor side effects — always return a cleanup function when subscribinguseMemocaches computed values,useCallbackcaches 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
| Hook | Purpose | Re-renders? |
|---|---|---|
| useState | Local state value | Yes, on state change |
| useReducer | Complex state machine | Yes, on dispatch |
| useEffect | Side effects (fetch, subscribe, DOM) | No |
| useLayoutEffect | Same as useEffect but fires sync before paint | No |
| useRef | Mutable ref (DOM or any value) | No |
| useMemo | Cache a computed value | No |
| useCallback | Cache a function reference | No |
| useContext | Read from a Context | Yes, on context value change |
| useId | Generate stable unique IDs | No |
| useTransition | Mark state update as non-urgent | No |
| useDeferredValue | Defer a value update (like debounce) | No |
| useImperativeHandle | Expose methods on a ref to parent | No |
| useDebugValue | Label custom hook in DevTools | No |
| use() (React 19) | Read a Promise or Context (anywhere) | Suspends |
| useOptimistic (React 19) | Show optimistic UI during async action | Yes |
| useFormStatus (React 19) | Read parent form’s submission state | Yes |
| useActionState (React 19) | Manage state driven by form actions | Yes |
useState
const [value, setValue] = useState(initialValue) // 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()) 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:
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
// 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 | [] deps | Notes |
|---|---|---|
| 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 render | omit array | Rare — usually a bug |
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:
// 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
// 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
// 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 use | When NOT to use |
|---|---|
| Passing callback to React.memo child | Callbacks called once, not passed down |
| Expensive array/object transformations | Simple value reads |
| Dependency of another hook | Every function by default |
useContext
// 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:
// 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 />} // 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
// 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
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
// 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
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*:
// 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
✅ 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.useEffectcleanup is mandatory for subscriptions, timers, and fetches.useRefdoes not cause re-renders — use it for DOM access and non-reactive values.useMemo/useCallbackare optimization tools, not defaults — measure first.- React 19’s
use(),useActionState,useFormStatus,useOptimisticmake 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).
What to read next
- TypeScript Cheat Sheet — type your hooks properly
- Next.js App Router Cheat Sheet — where React hooks meet Server Components
- Tailwind CSS Cheat Sheet — style the components you build
Related Articles
Deepen your understanding with these curated continuations.
Next.js App Router Cheat Sheet (Next.js 15, 2026)
Complete Next.js App Router reference — file conventions, Server vs Client Components, data fetching, Server Actions, metadata, and Pages Router migration table.
TypeScript Cheat Sheet: Types, Generics & Utilities
The essential TypeScript reference for primitive types, generics, and utility types. Learn type guards, mapped types, and optimal tsconfig configurations.
Tailwind CSS v4 Cheat Sheet: Layout, Flex & Grid
Complete Tailwind CSS v4 reference for layout, spacing, and typography. Master flexbox, grid, responsive design, and dark mode with this essential guide.