React Hooks Basics
11 examples to get you started with React Hooks -- 7 basic and 4 intermediate.
Prerequisites
Hooks ship with React itself -- no extra dependencies required. A TypeScript React project (Next.js, Vite, or CRA) is enough to run every example below.
# If you do not already have a project:
npx create-next-app@latest my-app --typescript --tailwind --app
cd my-app
npm run devTwo rules apply to every hook on this page:
- Only call hooks at the top level of a component or another hook -- never inside loops, conditions, or nested functions.
- Only call hooks from React function components or custom hooks, never from regular JavaScript functions.
Basic Examples
1. useState
Store a value that triggers a re-render whenever it changes.
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount((c) => c + 1)}>
Count: {count}
</button>
);
}useStatereturns a[value, setter]pair.- Calling the setter schedules a re-render with the new value.
- Use the updater form (
(c) => c + 1) when the new value depends on the previous one -- it avoids stale closures. - The argument to
useState(0)is the initial value, used only on first render.
Related: useState -- updater functions, lazy init, batching | Typing State -- typing complex state shapes
2. useEffect
Run a side effect after render -- subscribing, timing, or touching non-React APIs.
import { useEffect, useState } from "react";
function Clock() {
const [now, setNow] = useState(() => new Date());
useEffect(() => {
const id = setInterval(() => setNow(new Date()), 1000);
return () => clearInterval(id);
}, []);
return <p>{now.toLocaleTimeString()}</p>;
}- The effect runs after the DOM is painted, not during render.
- Return a cleanup function to tear down subscriptions, timers, or listeners.
- The dependency array
[]means "run once on mount"; listing variables means "re-run when any of them change". - Do not use
useEffectfor data you can fetch in a Server Component -- it causes waterfalls and loading flicker.
Related: useEffect -- cleanup, dependency arrays, common mistakes | SWR Basic Fetching -- prefer this for client-side data
3. useRef
Hold a value across renders without triggering a re-render, or reference a DOM node.
import { useEffect, useRef } from "react";
function AutoFocusInput() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} placeholder="Auto-focused" />;
}useRefreturns a{ current }object that persists across renders.- Mutating
ref.currentdoes not trigger a re-render -- unlike state. - Pass the ref to the
refprop to get a handle to the underlying DOM element. - Typical uses: DOM access, timer IDs you need to clear, storing the latest value of a prop or callback.
Related: useRef -- ref patterns, forwarding, callback refs | Typing Refs -- ref types for elements and instances
4. useContext
Read a context value anywhere in the tree without prop drilling.
import { createContext, useContext } from "react";
const ThemeContext = createContext<"light" | "dark">("light");
function ThemedLabel() {
const theme = useContext(ThemeContext);
return (
<span className={theme === "dark" ? "text-white" : "text-black"}>Hi</span>
);
}
function App() {
return (
<ThemeContext.Provider value="dark">
<ThemedLabel />
</ThemeContext.Provider>
);
}- Every call to
useContextsubscribes the component to the context -- it re-renders whenever the provider'svaluechanges. - The default value (
"light"here) is used only when no matching provider is above in the tree. - For frequently updated state, a dedicated store avoids re-rendering every consumer.
Related: useContext -- providers, default values, splitting contexts | Context Patterns -- when and how to split contexts | Context vs. Zustand -- choosing between them
5. useReducer
Manage complex state transitions with action-based updates.
import { useReducer } from "react";
type Action = { type: "increment" } | { type: "decrement" } | { type: "reset" };
function reducer(state: number, action: Action): number {
switch (action.type) {
case "increment":
return state + 1;
case "decrement":
return state - 1;
case "reset":
return 0;
}
}
function Counter() {
const [count, dispatch] = useReducer(reducer, 0);
return (
<div>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<span>{count}</span>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
</div>
);
}- Reach for
useReducerwhen state has multiple related fields or complex transitions that would span manyuseStatecalls. - The reducer is a pure function -- same input, same output, no side effects.
- Typing actions as a discriminated union lets TypeScript catch invalid
dispatchcalls. - If you are using more than 3-4
useStatecalls in one component, consideruseReducerinstead.
Related: useReducer -- action patterns, lazy init, nested state | Discriminated Unions -- type-safe action shapes
6. useMemo
Memoize an expensive computation so it only re-runs when its inputs change.
import { useMemo, useState } from "react";
function ProductList({ products }: { products: { id: number; price: number }[] }) {
const [filter, setFilter] = useState("");
const total = useMemo(
() => products.reduce((sum, p) => sum + p.price, 0),
[products]
);
return (
<div>
<input value={filter} onChange={(e) => setFilter(e.target.value)} />
<p>Total: ${total}</p>
</div>
);
}useMemorecomputes the value only when dependencies change, skipping work on unrelated re-renders.- Use it for expensive calculations or to keep referential identity stable for props passed to memoized children.
- Do not wrap every value -- premature
useMemoadds code overhead without perf gain. - With the React Compiler (React 19+), many
useMemocalls become unnecessary -- the compiler memoizes automatically.
Related: useMemo -- when memoization actually helps | React Compiler -- auto-memoization in React 19 | Memoization -- broader perf patterns
7. useCallback
Memoize a function reference so it stays stable across renders.
import { useCallback, useState } from "react";
function SearchBox({ onSearch }: { onSearch: (q: string) => void }) {
return <input onChange={(e) => onSearch(e.target.value)} />;
}
function App() {
const [query, setQuery] = useState("");
const handleSearch = useCallback((q: string) => {
setQuery(q);
}, []);
return (
<>
<SearchBox onSearch={handleSearch} />
<p>Query: {query}</p>
</>
);
}useCallback(fn, deps)is equivalent touseMemo(() => fn, deps).- Useful when passing a callback to a memoized child (
React.memo) or listing it as a dependency inuseEffect. - Without memoization, every re-render creates a new function reference and invalidates children and effects.
- Like
useMemo, avoid premature use -- the React Compiler handles most cases in React 19.
Related: useCallback -- patterns and pitfalls | useMemo -- sibling primitive | Re-renders -- when callback identity matters
Intermediate Examples
8. Custom Hook
Extract stateful logic into a reusable function.
import { useEffect, useState } from "react";
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(() =>
typeof navigator === "undefined" ? true : navigator.onLine
);
useEffect(() => {
const on = () => setIsOnline(true);
const off = () => setIsOnline(false);
window.addEventListener("online", on);
window.addEventListener("offline", off);
return () => {
window.removeEventListener("online", on);
window.removeEventListener("offline", off);
};
}, []);
return isOnline;
}
function StatusBanner() {
const isOnline = useOnlineStatus();
return <p>You are {isOnline ? "online" : "offline"}</p>;
}- A custom hook is any function whose name starts with
useand may call other hooks. - It lets you share logic between components without a wrapper component or higher-order component.
- Each component that calls the hook gets its own independent state -- nothing is shared across call sites.
- Handle SSR carefully -- browser-only APIs (
navigator,window) need guards for the server render.
Related: Custom Hooks -- rules, testing, patterns | Custom Hooks Guide -- broader patterns | useToggle -- a real-world custom hook
9. useTransition
Mark a state update as non-urgent so the UI stays responsive.
import { useState, useTransition } from "react";
function FilterableList({ items }: { items: string[] }) {
const [query, setQuery] = useState("");
const [filtered, setFiltered] = useState(items);
const [isPending, startTransition] = useTransition();
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value);
startTransition(() => {
setFiltered(items.filter((i) => i.includes(e.target.value)));
});
};
return (
<div>
<input value={query} onChange={handleChange} />
{isPending && <p>Updating...</p>}
<ul>
{filtered.map((i) => (
<li key={i}>{i}</li>
))}
</ul>
</div>
);
}- Updates inside
startTransitionare low priority -- React can interrupt them if the user types again. isPendinglets you show a subtle loading indicator during the deferred work.- Use it for heavy filtering, sorting, or chart updates that would otherwise block typing.
- React 19 wraps
<form action={...}>submissions in a transition automatically -- no explicitstartTransitionneeded for form actions.
Related: useTransition -- patterns and pitfalls | useDeferredValue -- sibling hook for deferring a value rather than an update
10. useActionState (React 19)
Drive a form action and track its pending state and result in one call.
"use client";
import { useActionState } from "react";
async function submitFeedback(_prev: string | null, formData: FormData) {
const message = formData.get("message") as string;
if (!message) return "Message is required.";
await fetch("/api/feedback", { method: "POST", body: formData });
return null;
}
function FeedbackForm() {
const [error, action, isPending] = useActionState(submitFeedback, null);
return (
<form action={action}>
<textarea name="message" />
{error && <p>{error}</p>}
<button type="submit" disabled={isPending}>
{isPending ? "Sending..." : "Send"}
</button>
</form>
);
}- Returns
[state, action, isPending]-- wire theactiondirectly to<form action={action}>. - The action function receives the previous state and the
FormData; its return value becomes the next state. - Pair with server actions for progressive enhancement -- the form works even before JS loads.
- Renamed from React 18's
useFormState-- same API, new name.
Related: useActionState -- full API and patterns | Server Actions -- the server side of the pair | Server Action Forms -- end-to-end form patterns
11. use (React 19)
Read a promise or context inline -- no .then, no useEffect.
"use client";
import { Suspense, use } from "react";
interface User {
id: number;
name: string;
}
function UserCard({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise);
return <h2>{user.name}</h2>;
}
function UserPage({ userPromise }: { userPromise: Promise<User> }) {
return (
<Suspense fallback={<p>Loading...</p>}>
<UserCard userPromise={userPromise} />
</Suspense>
);
}use(promise)suspends the component until the promise resolves -- the parent<Suspense>shows the fallback.use(context)reads a context value and, unlikeuseContext, can be called conditionally insideifblocks.- Create the promise in a parent (often a Server Component) and pass it down -- do not create it inside the rendering component, or it will be re-created every render.
- Combine with streaming Server Components for a no-
useEffectdata fetching story.
Related: use -- full API details | Suspense -- the fallback mechanism | Server Components -- where promises usually originate