React + Next.js

Effects, Data Fetching and Custom Hooks

35 min

useEffect: syncing with the outside world

Effects run after render, for things outside React: fetching data, subscriptions, timers, browser APIs.

import { useEffect, useState } from "react";

function Repos({ user }) {
  const [repos, setRepos] = useState([]);
  const [status, setStatus] = useState("loading");

  useEffect(() => {
    const controller = new AbortController();
    setStatus("loading");
    fetch(`https://api.github.com/users/${user}/repos`, { signal: controller.signal })
      .then(r => { if (!r.ok) throw new Error(r.status); return r.json(); })
      .then(data => { setRepos(data); setStatus("done"); })
      .catch(err => { if (err.name !== "AbortError") setStatus("error"); });
    return () => controller.abort();          // cleanup: cancel if user changes or unmounts
  }, [user]);                                  // re-run when user changes

  if (status === "loading") return <p>Loading…</p>;
  if (status === "error") return <p>Could not load repositories.</p>;
  return <ul>{repos.map(r => <li key={r.id}>{r.name}</li>)}</ul>;
}
  • The dependency array lists values the effect uses; React re-runs it when they change.
  • The cleanup function prevents updates from stale requests and memory leaks.
  • Do not use effects to compute values from props or state; calculate them during render.

Custom hooks

Extract reusable logic into functions starting with use:

function useFetch(url) {
  const [state, setState] = useState({ data: null, error: null, loading: true });
  useEffect(() => {
    const c = new AbortController();
    fetch(url, { signal: c.signal })
      .then(r => r.ok ? r.json() : Promise.reject(r.status))
      .then(data => setState({ data, error: null, loading: false }))
      .catch(error => error?.name !== "AbortError" && setState({ data: null, error, loading: false }));
    return () => c.abort();
  }, [url]);
  return state;
}

Data libraries

For real apps, use TanStack Query (or SWR): caching, deduplication, background refresh and retries for free.

const { data, isPending, error } = useQuery({
  queryKey: ["projects", page],
  queryFn: () => fetch(`/api/projects?page=${page}`).then(r => r.json()),
});

Other hooks you will meet

  • useRef: a mutable value or DOM reference that does not trigger re-renders (focusing inputs).
  • useMemo / useCallback: memoise expensive calculations or callbacks, only when profiling shows a need.
  • useContext: share data (theme, current user) without passing props through every level.

Exercise

Build a GitHub profile viewer with a useFetch hook: input a username, show avatar, bio and top repositories, with loading and error states and request cancellation when the username changes.

Check yourself

  • What does the dependency array control?
  • Why return a cleanup function from an effect?
  • When should you not use an effect?

Your progress is saved in this browser only.

Submit for review (10 points)