Introduction to React Hooks: Tutorial with Code Examples

Hooks are a set of APIs for handling state, side effects, context, memoization, external stores, and DOM integration in function components. Assuming React 18 and later, this guide explains practical usage, design tips, pitfalls, and performance considerations for Hooks, along with JavaScript code samples.

useState — Minimal state management and functional updates

Using functional updates ensures you always compute the next value from the latest state, preventing stale values during burst updates or async flows. Also, whenever you use side effects like timers or event listeners in useEffect, make sure to clean them up when the component unmounts.

jsx
import { useEffect, useState } from "react";

export function Counter() {
  const [count, setCount] = useState(0);

  const inc = () => setCount((c) => c + 1);

  useEffect(() => {
    const id = setInterval(() => setCount((c) => c + 1), 1000);
    return () => clearInterval(id);
  }, []);

  return <button onClick={inc}>Count: {count}</button>;
}

useEffect — Side effects, cleanup, and dependency array principles

For tasks like data fetching, make them cancelable when they become unnecessary. Also, include all values and functions used inside the effect in its dependency array to avoid bugs from missing dependencies and unnecessary reruns.

jsx
import { useEffect, useState } from "react";

export function UsersList() {
  const [users, setUsers] = useState(null);
  const [error, setError] = useState(null);

    useEffect(() => {
    const ac = new AbortController();

    (async () => {
      try {
        setError(null);
        const res = await fetch("/api/users", { signal: ac.signal });
        if (!res.ok) throw new Error("Network error");
        const data = await res.json();
        setUsers(data);
      } catch (e) {
        if (e.name !== "AbortError") setError(e);
      }
    })();

    return () => ac.abort();
  }, []);

  if (error) return <p role="alert">{error.message}</p>;
  if (!users) return <p>Loading...</p>;

  return (
    <ul>
      {users.map((u) => (
        <li key={u.id}>{u.name}</li>
      ))}
    </ul>
  );
}

useRef — A mutable box and DOM references

Use it to hold values without causing re-renders, or to directly access DOM elements. A small custom hook to keep the previous value is also common.

jsx
import { useEffect, useRef, useState } from "react";

export function SearchBox() {
  const inputRef = useRef(null);
  const [q, setQ] = useState("");

  useEffect(() => {
    inputRef.current?.focus();
  }, []);

  return (
    <label>
      Query:
      <input ref={inputRef} value={q} onChange={(e) => setQ(e.target.value)} />
    </label>
  );
}
jsx
import { useEffect, useRef } from "react";

export function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  }, [value]);
  return ref.current;
}

useMemo / useCallback — Prevent unnecessary work and re-creation

Use these selectively for expensive computations, filtering large arrays, and when you need to pass memoized callbacks through props.

jsx
import { memo, useCallback, useMemo, useState } from "react";

const Row = memo(function Row({ item, onSelect }) {
  return <li onClick={() => onSelect(item.id)}>{item.label}</li>;
});

export function FilteredList({ items }) {
  const [q, setQ] = useState("");

  const filtered = useMemo(() => {
    const lower = q.toLowerCase();
    return items.filter((i) => i.label.toLowerCase().includes(lower));
  }, [items, q]);

  const handleSelect = useCallback((id) => {
    console.log("selected", id);
  }, []);

  return (
    <>
      <input
        placeholder="search"
        value={q}
        onChange={(e) => setQ(e.target.value)}
      />
      <ul>
        {filtered.map((i) => (
          <Row key={i.id} item={i} onSelect={handleSelect} />
        ))}
      </ul>
    </>
  );
}

useReducer — For complex state and event-driven updates

Collecting state transitions in a reducer clarifies intent and makes testing easier.

jsx
import { useReducer, useState } from "react";

function uid() {
  return Math.random().toString(36).slice(2, 10);
}

function reducer(state, action) {
  switch (action.type) {
    case "add":
      return [...state, { id: uid(), text: action.text, done: false }];
    case "toggle":
      return state.map((t) =>
        t.id === action.id ? { ...t, done: !t.done } : t
      );
    case "remove":
      return state.filter((t) => t.id !== action.id);
    default:
      return state;
  }
}

export function TodoApp() {
  const [todos, dispatch] = useReducer(reducer, []);
  const [text, setText] = useState("");

  return (
    <>
      <form
        onSubmit={(e) => {
          e.preventDefault();
          if (text.trim()) dispatch({ type: "add", text });
          setText("");
        }}
      >
        <input
          value={text}
          onChange={(e) => setText(e.target.value)}
          placeholder="Add todo"
        />
        <button>Add</button>
      </form>
      <ul>
        {todos.map((t) => (
          <li key={t.id}>
            <label>
              <input
                type="checkbox"
                checked={t.done}
                onChange={() => dispatch({ type: "toggle", id: t.id })}
              />
              {t.text}
            </label>
            <button onClick={() => dispatch({ type: "remove", id: t.id })}>
              x
            </button>
          </li>
        ))}
      </ul>
    </>
  );
}

useContext — Make global dependencies explicit

Providing a custom read hook makes it easier to use. It also clarifies dependencies and encourages a loosely coupled design.

jsx
import { createContext, useContext, useMemo, useState } from "react";

const ThemeContext = createContext(null);

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("light");
  const value = useMemo(() => ({ theme, setTheme }), [theme]);
  return (
    <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
  );
}

export function useTheme() {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error("useTheme must be inside ThemeProvider");
  return ctx;
}

Concurrent rendering — useTransition and useDeferredValue

When you want to prioritize input responsiveness while deferring heavy rendering, use useTransition. If you only need to delay a value, useDeferredValue is simpler.

jsx
import { useState, useTransition } from "react";

export function SearchWithTransition() {
  const [input, setInput] = useState("");
  const [query, setQuery] = useState("");
  const [isPending, startTransition] = useTransition();

  const onChange = (v) => {
    setInput(v);
    startTransition(() => setQuery(v));
  };

  return (
    <>
      <input
        value={input}
        onChange={(e) => onChange(e.target.value)}
        placeholder="search"
      />
      {isPending && <p>Updating...</p>}
      <LargeList filter={query} />
    </>
  );
}

function LargeList({ filter }) {
  return <div>Filtering by: {filter}</div>;
}
jsx
import { useDeferredValue, useMemo, useState } from "react";

export function SearchWithDeferred({ items }) {
  const [q, setQ] = useState("");
  const deferredQ = useDeferredValue(q);

  const results = useMemo(() => {
    return items.filter((s) => s.includes(deferredQ));
  }, [items, deferredQ]);

  return (
    <>
      <input value={q} onChange={(e) => setQ(e.target.value)} />
      <div>{results.length} items</div>
    </>
  );
}

useId — Stable associations across SSR and CSR

Generate stable IDs that match between the server and the client for form labels and ARIA relationships.

jsx
import { useId } from "react";

export function LabeledInput() {
  const id = useId();
  return (
    <div>
      <label htmlFor={id}>Name</label>
      <input id={id} />
    </div>
  );
}

useSyncExternalStore — Consistent subscription to external stores

This example subscribes to a browser API’s state. You can provide a snapshot for SSR.

jsx
import { useSyncExternalStore } from "react";

function subscribe(cb) {
  window.addEventListener("online", cb);
  window.addEventListener("offline", cb);
  return () => {
    window.removeEventListener("online", cb);
    window.removeEventListener("offline", cb);
  };
}

function getSnapshot() {
  return navigator.onLine;
}

function getServerSnapshot() {
  return true;
}

export function OnlineBadge() {
  const online = useSyncExternalStore(
    subscribe,
    getSnapshot,
    getServerSnapshot
  );
  return <span>{online ? "Online" : "Offline"}</span>;
}

useLayoutEffect — When DOM measurement or synchronous layout is needed

Runs synchronously after DOM mutations and before the browser paints. Use it for measurements or preventing flicker. Prefer useEffect when possible.

jsx
import { useLayoutEffect, useRef, useState } from "react";

export function MeasureBox() {
  const ref = useRef(null);
  const [rect, setRect] = useState(null);

  useLayoutEffect(() => {
    if (ref.current) setRect(ref.current.getBoundingClientRect());
  }, []);

  return (
    <div>
      <div
        ref={ref}
        style={{ width: 200, height: 120, background: "#eef" }}
      />
      <pre>{rect ? JSON.stringify(rect.toJSON()) : "measuring..."}</pre>
    </div>
  );
}

forwardRef + useImperativeHandle — Expose an imperative API from a child

Safely export imperative operations such as focus and scroll.

jsx
import { forwardRef, useImperativeHandle, useRef, useState } from "react";

export const TextInput = forwardRef(function TextInput({ initial = "" }, ref) {
  const inputRef = useRef(null);
  const [value, setValue] = useState(initial);

  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current?.focus(),
    clear: () => setValue(""),
  }));

  return (
    <input
      ref={inputRef}
      value={value}
      onChange={(e) => setValue(e.target.value)}
    />
  );
});
jsx
import { useRef } from "react";
import { TextInput } from "./TextInput";

export function ImperativeExample() {
  const ref = useRef(null);
  return (
    <div>
      <TextInput ref={ref} />
      <button onClick={() => ref.current?.focus()}>Focus</button>
      <button onClick={() => ref.current?.clear()}>Clear</button>
    </div>
  );
}

Common pitfalls

  • Missing items in dependency arrays are a major source of bugs. Creating objects or functions inline on every render destabilizes dependencies and can trigger excessive reruns. Use useMemo and useCallback to stabilize references when needed.
  • You don’t need useEffect for purely render-time derived values. If a derived value can be computed during render, avoid state—this helps prevent loops and consistency issues.
  • Updating a value stored in useRef does not cause a re-render. Use state if you want UI to update. Conversely, use a ref for ephemeral data you don’t want to trigger re-renders.

Summary

Hooks become easier to design around when you master: state management (useState/useReducer), side effects (useEffect), references (useRef), memoization (useMemo/useCallback), context (useContext), concurrent UI (useTransition/useDeferredValue), external stores (useSyncExternalStore), and DOM synchronization (useLayoutEffect).

69

Recommended