React SME Cookbook
All FAQs
basicsreact-hookshooksexamples

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 dev

Two rules apply to every hook on this page:

  1. Only call hooks at the top level of a component or another hook -- never inside loops, conditions, or nested functions.
  2. 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>
  );
}
  • useState returns 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 useEffect for 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" />;
}
  • useRef returns a { current } object that persists across renders.
  • Mutating ref.current does not trigger a re-render -- unlike state.
  • Pass the ref to the ref prop 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 useContext subscribes the component to the context -- it re-renders whenever the provider's value changes.
  • 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 useReducer when state has multiple related fields or complex transitions that would span many useState calls.
  • The reducer is a pure function -- same input, same output, no side effects.
  • Typing actions as a discriminated union lets TypeScript catch invalid dispatch calls.
  • If you are using more than 3-4 useState calls in one component, consider useReducer instead.

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>
  );
}
  • useMemo recomputes 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 useMemo adds code overhead without perf gain.
  • With the React Compiler (React 19+), many useMemo calls 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 to useMemo(() => fn, deps).
  • Useful when passing a callback to a memoized child (React.memo) or listing it as a dependency in useEffect.
  • 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 use and 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 startTransition are low priority -- React can interrupt them if the user types again.
  • isPending lets 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 explicit startTransition needed 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 the action directly 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, unlike useContext, can be called conditionally inside if blocks.
  • 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-useEffect data fetching story.

Related: use -- full API details | Suspense -- the fallback mechanism | Server Components -- where promises usually originate