FUNSTACK Router

HomeGetting StartedLearnAPI ReferenceExamplesFAQ
GitHub

Built with @funstack/router — A modern React router based on the Navigation API

Learn

Navigation APINested RoutesType SafetyForm ActionsHow Loaders RunError HandlingTransitionsSSRHow SSR WorksStatic Site GenerationSSR with LoadersRSCReact Server ComponentsRSC with Route Features

How Loaders Run

Loaders fetch data for a route before the UI renders. This page explains when loaders execute, how results are cached, and how different types of navigation affect loader behavior.

Defining a Loader

A loader is a function on a route definition that receives the route params, a Request, and an AbortSignal. It can return any value — typically a Promise from a fetch call:

import { route } from "@funstack/router";

const userRoute = route({
  path: "/users/:id",
  loader: async ({ params, request, signal }) => {
    const res = await fetch(`/api/users/${params.id}`, { signal });
    return res.json();
  },
  component: UserPage,
});

The component receives the loader’s return value as the data prop. For async loaders this is a Promise, which you unwrap with React’s use() hook inside a Suspense boundary:

import { use, Suspense } from "react";

function UserPage({ data }: { data: Promise<User> }) {
  return (
    <Suspense fallback={<p>Loading...</p>}>
      <UserDetail data={data} />
    </Suspense>
  );
}

function UserDetail({ data }: { data: Promise<User> }) {
  const user = use(data);
  return <h1>{user.name}</h1>;
}

When Loaders Execute

Loaders run at two points in the lifecycle:

  1. Initial page load — When the Router first renders, it matches the current URL against the route definitions and executes all matching loaders immediately.
  2. Navigation events — When the user navigates (by clicking a link, submitting a form, or calling navigate()), the Router matches the destination URL and executes loaders for the matched routes.

In both cases, all loaders in the matched route stack (parent and child) run in parallel. The navigation completes once every loader’s Promise has resolved.

Caching by Navigation Entry

Loader results are cached using the navigation entry ID from the Navigation API. Each time you navigate to a new URL, the browser creates a new navigation entry with a unique ID. The Router uses this ID as the cache key, so:

  • Re-renders of the same page do not re-execute loaders — the cached result is returned.
  • Navigating to a new URL always creates a new entry and always executes loaders, even if the URL is the same as a previous navigation.

This design ensures that loaders run exactly once per navigation while preventing unnecessary re-fetches during React re-renders.

Navigation Types and Loader Behavior

Different types of navigation have different effects on whether loaders run:

Push and Replace

A push navigation (the default when clicking a link or calling navigate()) creates a new navigation entry. Since the entry is new, loaders always execute. A replace navigation behaves the same way — it creates a new entry that replaces the current one, so loaders execute fresh.

Traverse (Back / Forward)

When the user goes back or forward in history, the browser revisits an existing navigation entry. Because the entry ID is the same as when the page was originally visited, the cached loader results are returned without re-executing the loaders. This makes back/forward navigation instant.

Reload

A reload navigation stays on the same navigation entry, but the Router generates a fresh cache key so that all loaders re-execute. This is useful when you want to refresh data without navigating away from the current page.

You can trigger a reload programmatically using the Navigation API’s navigation.reload() method:

function RefreshButton() {
  return (
    <button onClick={() => navigation.reload()}>
      Refresh Data
    </button>
  );
}

During a reload, the old cached data remains available for the pending UI. Because the Router wraps navigations in a React transition, the previous UI stays on screen while the new data loads. Once the new loaders resolve, the UI updates. This means users see the existing content while the refresh is in progress, rather than a blank screen or loading spinner.

Consecutive reloads work correctly — each reload increments an internal counter to produce a unique cache key, and stale caches are pruned automatically.

Form Submissions

When a <form method="post"> is submitted, the Router runs the matched route’s action first, then clears the loader cache for the current entry and re-executes all loaders. The action’s return value is passed to each loader as actionResult. See the Form Actions page for details.

Nested Route Loaders

When routes are nested, each route in the matched stack can define its own loader. All loaders in the stack execute in parallel, and each component receives its own loader’s result:

const routes = [
  route({
    path: "/dashboard",
    loader: () => fetchDashboardLayout(),
    component: DashboardLayout,
    children: [
      route({
        path: "/stats",
        loader: () => fetchStats(),
        component: StatsPage,
      }),
    ],
  }),
];

// When navigating to /dashboard/stats:
// → fetchDashboardLayout() and fetchStats() run in parallel
// → DashboardLayout receives the layout data
// → StatsPage receives the stats data

On reload, all loaders in the matched stack re-execute, not just the deepest one.

Cache Cleanup

Cached loader results are automatically cleaned up when a navigation entry is disposed. The browser disposes entries when they are removed from the history stack (for example, when the user navigates forward from a point in the middle of the history stack, the entries ahead are discarded). The Router listens for these dispose events and removes the corresponding cached data.

Error Handling

When a loader throws, the error bubbles to the nearest Error Boundary above that route. Synchronous loader errors are re-thrown during route rendering, while rejected loader Promises surface when use(data) runs.

For the recommended boundary placement, how to reset it on navigation with useLocation().entryId, and examples for nested layouts, see Error Handling.

Summary

Navigation typeLoaders run?Why
Push / ReplaceYesNew navigation entry, no cache
Traverse (Back / Forward)NoExisting entry, cached results returned
ReloadYesFresh cache key generated
Form submission (POST)YesCache cleared after action runs