Real World Example

Typed Data Fetching Hook

Dashboards (Razorpay Dashboard, Vercel, Notion) fetch data in many components. A generic hook returns a discriminated union state, so components must handle loading and error before touching the data.

"use client";
import { useEffect, useState } from "react";

type AsyncState<T> =
  | { status: "loading" }
  | { status: "error"; error: string }
  | { status: "success"; data: T };

export function useFetch<T>(url: string, parse: (json: unknown) => T): AsyncState<T> {
  const [state, setState] = useState<AsyncState<T>>({ status: "loading" });

  useEffect(() => {
    const controller = new AbortController();
    setState({ status: "loading" });
    fetch(url, { signal: controller.signal })
      .then((res) => (res.ok ? res.json() : Promise.reject(new Error("HTTP " + res.status))))
      .then((json: unknown) => setState({ status: "success", data: parse(json) }))
      .catch((e: unknown) => {
        if (!controller.signal.aborted) setState({ status: "error", error: e instanceof Error ? e.message : "Unknown error" });
      });
    return () => controller.abort();
  }, [url, parse]);

  return state;
}

type Order = { id: string; total: number; status: "PLACED" | "SHIPPED" | "DELIVERED" };
const parseOrders = (json: unknown) => json as Order[]; // use a Zod schema in real code

export function RecentOrders() {
  const state = useFetch("/api/orders?limit=5", parseOrders);

  if (state.status === "loading") return <p>Loading orders...</p>;
  if (state.status === "error") return <p role="alert">Could not load orders: {state.error}</p>;

  return (
    <ul>
      {state.data.map((o) => (
        <li key={o.id}>{o.id} - Rs. {o.total} - {o.status}</li>
      ))}
    </ul>
  );
}