FUNSTACK Router

HomeGetting StartedLearnAPI ReferenceExamplesFAQ
GitHub

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

Getting Started

  • Getting Started
  • Examples

Learn

  • Overview
  • Navigation API
  • Nested Routes
  • Type Safety
  • Form Actions
  • How Loaders Run
  • Error Handling
  • Transitions
  • SSR
    • How SSR Works
    • Static Site Generation
    • SSR with Loaders
  • RSC
    • React Server Components
    • RSC with Route Features

API Reference

  • Overview
  • Components
  • Hooks
  • Utilities
  • Types

Resources

  • FAQ
  • GitHub ↗

API Reference

ComponentsHooksUtilitiesTypes

Utilities

Helper functions for defining routes and managing state.

route()

Helper function to define routes with proper typing. The component always receives a params prop with types inferred from the path pattern. When a loader is defined, the component also receives a data prop. Components also receive state, setState, setStateSync, resetState, and resetStateSync props for navigation state management.

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

// Route without loader - component receives params prop
function ProfileTab({ params }: { params: { userId: string } }) {
  return <div>Profile for user {params.userId}</div>;
}

// Route with loader - component receives both data and params props
function UserPage({
  data,
  params,
}: {
  data: User;
  params: { userId: string };
}) {
  return <h1>{data.name} (ID: {params.userId})</h1>;
}

const myRoute = route({
  path: "/users/:userId",
  component: UserPage,
  loader: async ({ params }) => {
    return fetchUser(params.userId);
  },
  children: [
    route({ path: "/profile", component: ProfileTab }),
    route({ path: "/settings", component: SettingsTab }),
  ],
});

Options

OptionTypeDescription
pathstring (optional)URL path pattern (supports :param syntax). If omitted, creates a pathless route that always matches and consumes no pathname. Useful for layout wrappers.
componentComponentTypeReact component to render. Receives params prop (and data prop if loader is defined)
action(args: ActionArgs) => TFunction to handle form submissions (POST navigations). Receives a Request with FormData body. The return value is passed to the loader as actionResult.
loader(args: LoaderArgs) => TFunction to load data. May be synchronous or asynchronous
childrenRouteDefinition[]Nested child routes
exactbooleanOverride default matching. true = exact match only, false = prefix match. Defaults to true for leaf routes, false for parent routes.
requireChildrenbooleanWhether a parent route requires a child to match. true (default) = parent only matches if a child matches, false = parent can match alone with outlet as null. Enables catch-all routes to work intuitively.

routeState<TState>()

Curried helper function for defining routes with typed navigation state. Use this when your route component needs to manage state that persists across browser back/forward navigation.

import { routeState, type RouteComponentProps } from "@funstack/router";

type PageState = { scrollPosition: number; selectedTab: string };

function UserPage({
  params,
  state,
  setState,
  setStateSync,
  resetState,
}: RouteComponentProps<{ userId: string }, PageState>) {
  // state is PageState | undefined (undefined on first visit)
  const scrollPosition = state?.scrollPosition ?? 0;
  const selectedTab = state?.selectedTab ?? "posts";

  // Async state update (recommended for most cases)
  const handleTabChange = async (tab: string) => {
    await setState({ scrollPosition, selectedTab: tab });
  };

  // Sync state update (for immediate updates like scroll position)
  const handleScroll = (position: number) => {
    setStateSync({ scrollPosition: position, selectedTab });
  };

  return (
    <div>
      <h1>User {params.userId}</h1>
      <button onClick={() => resetState()}>Clear State</button>
    </div>
  );
}

// Use routeState<TState>() for typed state management
const userRoute = routeState<PageState>()({
  path: "/users/:userId",
  component: UserPage,
});

// With loader
const productRoute = routeState<{ filter: string }>()({
  path: "/products",
  component: ProductList,
  loader: async () => fetchProducts(),
});

How It Works

Navigation state is stored in the browser's Navigation API. The router provides two methods to update state:

  • setState - Async method that uses replace navigation. Returns a Promise that resolves when the navigation completes. Because it performs a navigation, the update is wrapped in a React transition (may set isPending to true).
  • setStateSync - Sync method that uses navigation.updateCurrentEntry(). Updates state immediately without waiting. This is not a navigation, so it bypasses React transitions entirely (isPending stays false).

Navigation state characteristics:

  • State persists when navigating back/forward in history
  • Each history entry has its own independent state
  • State must be serializable (no functions, Symbols, or DOM nodes)
  • State updates never re-execute loaders — loaders cannot observe navigation state, and cached loader results are preserved across setState’s same-URL replace navigation

Internal Storage

The router stores state internally using a __routeStates array indexed by route match position. This enables each nested route to maintain independent state:

// Internal structure stored in NavigationHistoryEntry
{
  __routeStates: [
    { sidebarOpen: true },    // Layout (index 0)
    { selectedTab: "posts" }, // UserPage (index 1)
    { scrollY: 500 },         // PostsPage (index 2)
  ]
}

hardReload()

Performs a full page reload, bypassing the router's navigation interception entirely. This is useful when you need a true browser reload that the router should not intercept.

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

// Full page reload — bypasses the router and all blockers
hardReload();

hardNavigate(url)

Performs a full page navigation to the given URL, bypassing the router's navigation interception entirely. This triggers a real browser navigation instead of a client-side route change, and skips all blockers.

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

// Full page navigation — bypasses the router and all blockers
hardNavigate("/other-page");

Parameters

ParameterTypeDescription
urlstringThe URL to navigate to

Server Entry Point

The route() and routeState() helpers are also available from a server-compatible entry point. Use this when defining routes in React Server Components or other server-side code.

// In Server Components or server-side route definitions
import { route, routeState } from "@funstack/router/server";

// Define routes without the "use client" directive
const routes = [
  route({
    path: "/",
    component: HomePage,
  }),
  routeState<{ tab: string }>()({
    path: "/dashboard",
    component: DashboardPage,
  }),
];

When to Use

  • Defining routes in React Server Components
  • Server-side route configuration files
  • Any context where "use client" would cause issues

Available Exports

The @funstack/router/server entry point exports:

  • route - Route definition helper
  • routeState - Route definition helper with typed state
  • bindRoute - Binds a component to a partial route definition (see Two-Phase Route Definitions)
  • Types: ActionArgs, LoaderArgs, RouteDefinition, PathParams, RouteComponentProps, RouteComponentPropsWithData

For client-side features like Router, Outlet, and hooks, use the main @funstack/router entry point.

See the React Server Components guide for a full walkthrough of using the server entry point, and the Two-Phase Route Definitions guide for using bindRoute() to split route definitions across the RSC boundary.