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

React Server Components

FUNSTACK Router is designed to work with React Server Components (RSC). The package provides a dedicated server entry point so that route definitions can live in server modules, keeping client bundle sizes small.

Why RSC Compatibility Matters

In an RSC architecture, the module graph is split into server modules and client modules. Server modules run at build time (or on the server) and are never sent to the browser. Client modules are marked with the "use client" directive and are included in the browser bundle.

The main @funstack/router entry point is marked "use client" because it exports components and hooks that depend on browser APIs (the Navigation API, React context, etc.). This means importing from @funstack/router in a server module would fail.

To solve this, the package provides a separate entry point: @funstack/router/server.

The @funstack/router/server Entry Point

The server entry point exports the route() and routeState() helper functions without the "use client" directive. This lets you define your route tree in a server module:

// App.tsx — a Server Component (no "use client" directive)
import { Router } from "@funstack/router";
import { route } from "@funstack/router/server";

const routes = [
  route({
    path: "/",
    component: Layout,
    children: [
      route({ path: "/", component: HomePage }),
      route({ path: "/about", component: AboutPage }),
    ],
  }),
];

export default function App() {
  return <Router routes={routes} />;
}

In this example, App is a server component. It builds the route array using route() from @funstack/router/server and renders the Router component from @funstack/router which is a client component.

What the server entry point exports

  • route — Route definition helper (same API as the main entry point)
  • routeState — Route definition helper with typed navigation state
  • Types: LoaderArgs, RouteDefinition, PathParams, RouteComponentProps, RouteComponentPropsWithData

Defining Routes in the Server Context

Route definitions can be defined in server modules because they are plain data structures except for page components and loader functions. Fortunately, it is possible to import both of these from client modules which results in client references that can be passed from the server to the client through the routes prop.

// App.tsx — Server Component
import { Router } from "@funstack/router";
import { route } from "@funstack/router/server";
import { lazy } from "react";

// Import page components from client modules
import HomePage from "./pages/HomePage.js";
import DashboardPage from "./pages/DashboardPage.js";
import SettingsPage from "./pages/SettingsPage.js";

const routes = [
  route({
    component: Layout,
    children: [
      route({ path: "/", component: HomePage }),
      route({ path: "/dashboard", component: DashboardPage }),
      route({ path: "/settings", component: SettingsPage }),
    ],
  }),
];

export default function App() {
  return <Router routes={routes} />;
}

Loaders in an RSC Context

Loaders run client-side — they execute in the browser when a route is matched. This means a loader function cannot be defined inline within a server module. Instead, define the loader in a client module and import it:

// loaders/dashboard.ts — a Client Module
"use client";

export async function dashboardLoader({ params }: LoaderArgs) {
  const res = await fetch(`/api/dashboard/${params.id}`);
  return res.json();
}
// App.tsx — Server Component
import { Router } from "@funstack/router";
import { route } from "@funstack/router/server";
import { dashboardLoader } from "./loaders/dashboard.js";

const routes = [
  route({
    component: Layout,
    children: [
      route({ path: "/", component: HomePage }),
      route({
        path: "/dashboard/:id",
        component: DashboardPage,
        loader: dashboardLoader,
      }),
    ],
  }),
];

export default function App() {
  return <Router routes={routes} />;
}

By placing the loader in a "use client" module, it is included in the client bundle where it can access browser APIs. The server component imports the reference and passes it as part of the route definition.

Using Server Components as Route Components

All examples so far have used client components as route components, but you can go even further and use server components as route components. Actually, this is the primary use case for the RSC support in FUNSTACK Router — it allows pre-rendering each route on the server (or at build time for static sites).

Use React Node as Route Components

When you use server components as route components, the route's component must be a React node (i.e. <MyComponent />) instead of a component reference (i.e. MyComponent) because a references to server components cannot be passed to the client.

// App.tsx — Server Component
import { Router } from "@funstack/router";
import { route } from "@funstack/router/server";
import HomePage from "./pages/HomePage.js";
import AboutPage from "./pages/AboutPage.js";

const routes = [
  route({
    component: <Layout />,
    children: [
      route({ path: "/", component: <HomePage /> }),
      route({ path: "/about", component: <AboutPage /> }),
    ],
  }),
];

export default function App() {
  return <Router routes={routes} />;
}

In this example, HomePage and AboutPage are server components. They are rendered on the server and the resulting HTML is sent to the client.

Due to this nature, a route component defined as a server component cannot receive route props (params, search params, navigation state, etc). We are exploring ways to lift this limitation in the future, but for now if you need to access route props you will need to use client components as route components.

For some use cases it is enough to have a client component child as a pathless route:

const routes = [
  route({
    path: "/",
    component: <HomePage />, // Server Component
    children: [
      route({
        component: InteractivePartOfHomePage, // Client Component
        loader: someLoaderForHomePage,
      }),
    ],
  }),
];

In this example, HomePage is a server component that renders the static parts of the page while InteractivePartOfHomePage is a client component that can access route props (like loader data). HomePage can render <Outlet /> to render its child routes.

Key Takeaways

  • Import route and routeState from @funstack/router/server to define routes in server modules
  • Router is a client component and serves as the client boundary — render it directly from your server component
  • Loaders run client-side — define them in "use client" modules and import them into your route definitions
  • Page components can be either server components or client components; if using server components, define them as React nodes (e.g. <MyPage />) instead of component references (e.g. MyPage)
  • See also the Server-Side Rendering guide for how the router handles SSR and hydration
  • For type-safe hooks in client components, see the RSC with Route Features guide which explains how to split route definitions across the server/client boundary