Why Data Routers Exist

Start with the code a declarative router forces you to write: mount, fire an Effect, paint a spinner while it waits.

Fetching in the component, the declarative wayJavaScript
function Product() {
  const { id } = useParams();
  const [product, setProduct] = useState(null);
  useEffect(() => {
    let live = true; setProduct(null);
    fetch(`/api/products/${id}`).then((r) => r.json())
      .then((d) => live && setProduct(d));
    return () => { live = false; };          // ignore an out-of-order response
  }, [id]);
  return product ? <h2>{product.name}</h2> : <p>Loading...</p>;
}

Eleven lines, one about products, and no error branch yet. The live flag guards against out-of-order responses (Out-of-Order Responses), the state exists because the component renders before its data, and the fetch starts after the route has rendered — behind any parent layout that fetches too. An Effect cannot run before its component exists, so that waterfall is structural.

A data router inverts the order: the route declares where its data comes from, the router fetches for every matched route at once, and the component becomes return <h2>{useLoaderData().name}</h2>;. The rest becomes router behavior — parallel loaders, useNavigation().state as the loading flag, route error boundaries, cancelled stale loads, revalidation after every write.

The costs are real. Route trees become objects rather than JSX, data lives per route, so two components needing the same record must sit under the route that loads it, and the router becomes the thing that knows your API — awkward if TanStack Query 79,069 (TanStack Query) already owns that role.