Hooks

Hooks let you use state, side effects, refs, and other React features inside function components. React 19 adds the use hook for reading Promises and Context anywhere, including inside conditionals.

Core Hooks

useState - Local State

const [count, setCount] = useState(0);
const [user, setUser] = useState<User | null>(null);

// Functional update when new state depends on old
setCount((prev) => prev + 1);

// Object state - always spread previous state
setUser((prev) => prev ? { ...prev, name: "New Name" } : prev);

Gotcha: useState uses Object.is for comparison. Setting the same value (same reference for objects) won’t trigger a re-render. But creating a new object {} with the same contents will.

useEffect - Side Effects

useEffect(() => {
  // Runs after render when `id` changes
  const controller = new AbortController();

  fetch(`/api/users/${id}`, { signal: controller.signal })
    .then((res) => res.json())
    .then(setUser);

  // Cleanup runs before next effect or on unmount
  return () => controller.abort();
}, [id]); // dependency array - empty = mount only, omit = every render
Dependency arrayWhen it runs
[a, b]After render if a or b changed
[]Once after initial render (mount)
OmittedAfter every render

Tip: In React 19 with the compiler, dependency arrays are validated at compile time. Missing deps become build errors, not subtle bugs.

useRef - Stable References

function TextInput() {
  const inputRef = useRef<HTMLInputElement>(null);

  function focusInput() {
    inputRef.current?.focus();
  }

  return <input ref={inputRef} />;
}

// Also useful for mutable values that don't trigger re-renders
const renderCount = useRef(0);
renderCount.current += 1; // no re-render

useMemo and useCallback - Memoization

// Memoize expensive computation
const sortedItems = useMemo(
  () => items.sort((a, b) => a.name.localeCompare(b.name)),
  [items]
);

// Memoize callback to prevent child re-renders
const handleClick = useCallback(
  (id: string) => setSelected(id),
  [] // no deps - setSelected is stable
);

Tip: With React Compiler enabled, you can delete useMemo and useCallback calls. The compiler inserts memoization automatically. See React Compiler.

useReducer - Complex State Logic

type State = { items: Item[]; loading: boolean; error: string | null };
type Action =
  | { type: "FETCH_START" }
  | { type: "FETCH_SUCCESS"; items: Item[] }
  | { type: "FETCH_ERROR"; error: string };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "FETCH_START":
      return { ...state, loading: true, error: null };
    case "FETCH_SUCCESS":
      return { items: action.items, loading: false, error: null };
    case "FETCH_ERROR":
      return { ...state, loading: false, error: action.error };
  }
}

const [state, dispatch] = useReducer(reducer, {
  items: [],
  loading: false,
  error: null,
});

dispatch({ type: "FETCH_START" });

The use Hook (React 19)

use is special. It reads the current value of a Promise or Context. Unlike other hooks, it works inside conditionals and loops.

Reading Promises

import { use, Suspense } from "react";

// The promise must be created OUTSIDE the component
// (in a parent, a loader, or a cache)
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise); // suspends until resolved
  return <h1>{user.name}</h1>;
}

// Parent wraps in Suspense
function Page() {
  const userPromise = fetchUser(userId); // start fetching

  return (
    <Suspense fallback={<Skeleton />}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  );
}

Gotcha: Don’t create the Promise inside the component that calls use. The component re-renders on suspend, which would create a new Promise, causing an infinite loop. Create it in a parent or cache it.

Reading Context (Conditional)

function StatusBadge({ showTheme }: { showTheme: boolean }) {
  // This was impossible with useContext - hooks can't be in conditionals
  // use() can be called conditionally
  if (showTheme) {
    const theme = use(ThemeContext);
    return <span style={{ color: theme.primary }}>Active</span>;
  }
  return <span>Active</span>;
}

use vs useEffect for Data Fetching

ApproachWhen to use
use(promise) + SuspenseFetching during render, data needed to display the component
useEffectSide effects that aren’t about fetching display data (analytics, subscriptions)
Server ComponentsBest option when you can fetch on the server
TanStack QueryComplex client-side caching, pagination, optimistic updates

Custom Hooks

Extract reusable logic into custom hooks. Any function starting with use is a hook.

function useLocalStorage<T>(key: string, initial: T) {
  const [value, setValue] = useState<T>(() => {
    const stored = localStorage.getItem(key);
    return stored ? JSON.parse(stored) : initial;
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue] as const;
}

// Usage
const [theme, setTheme] = useLocalStorage("theme", "dark");
function useDebounce<T>(value: T, delay: number): T {
  const [debounced, setDebounced] = useState(value);

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

  return debounced;
}

// Usage - only updates after 300ms of no typing
const debouncedSearch = useDebounce(searchTerm, 300);

Rules of Hooks

  1. Only call hooks at the top level - not inside loops, conditions, or nested functions (except use, which can be conditional)
  2. Only call hooks from React functions - components or other custom hooks, not regular functions
  3. Name custom hooks with use prefix - this lets React enforce the rules and lets the compiler optimize
// Bad - hook inside condition
function Bad({ loggedIn }: { loggedIn: boolean }) {
  if (loggedIn) {
    const [name, setName] = useState(""); // breaks rules
  }
}

// Good - always call the hook, conditionally use the value
function Good({ loggedIn }: { loggedIn: boolean }) {
  const [name, setName] = useState("");
  if (!loggedIn) return <Login />;
  return <span>{name}</span>;
}

// Exception: use() CAN be called conditionally
function AlsoGood({ loggedIn }: { loggedIn: boolean }) {
  if (loggedIn) {
    const user = use(userPromise); // this is fine
    return <span>{user.name}</span>;
  }
  return <Login />;
}

Tip: The eslint-plugin-react-hooks plugin (included in all React setups) catches rule violations at lint time. Don’t disable it.

Next: Server Components for the full server/client architecture