All skills
remix-run avatar

/react-router-framework-mode

@220978e
by Remixremix-run/agent-skills136 stars
4

Build full-stack React applications using React Router's framework mode. Use when configuring routes, working with loaders and actions, handling forms, handling navigation, pending/optimistic UI, error boundaries, or working with react-router.config.ts or other react router conventions.

Use this Skill: https://skilld.dev/gh/remix-run/agent-skills/react-router-framework-mode

This session only. Nothing lands on disk.

referencesspecial-files.md

≈3.1k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Special Files

React Router framework mode uses several special files with specific purposes.

Quick Reference

File Required Purpose
app/root.tsx Yes Root route rendering the HTML document
app/routes.ts Yes Route configuration (see routing.md)
react-router.config.ts No Framework configuration (SSR, prerender, etc.)
app/entry.client.tsx No Client-side hydration entry point
app/entry.server.tsx No Server-side rendering entry point
*.server.ts No Server-only modules (excluded from client)
*.client.ts No Client-only modules (excluded from server)

root.tsx (Required)

app/root.tsx is the only required route - it's the parent to all routes and renders the root <html> document.

What Belongs in root.tsx

Element Why
<html>, <head>, <body> Document structure
<Meta />, <Links /> Route meta/links aggregation
<Scripts />, <ScrollRestoration /> React Router runtime
<Outlet /> Child route rendering
Global navigation Appears on every page
Global footer Appears on every page
Context providers Available to all routes
Stylesheets/fonts Loaded once, cached
Global error boundary Catches app-wide errors
Loading indicators Show during navigation (see pending-ui.md)

Basic root.tsx Structure

import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";

export default function App() {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        <Outlet />
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

Using the Layout Export

The Layout export avoids duplicating the document shell across your component, HydrateFallback, and ErrorBoundary:

import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";

export function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        {children}
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

export default function App() {
  return <Outlet />;
}

export function ErrorBoundary() {
  return <div>Something went wrong</div>;
}

export function HydrateFallback() {
  return <div>Loading...</div>;
}

Customizing root.tsx (Complete Example)

import {
  Links,
  Meta,
  NavLink,
  Outlet,
  Scripts,
  ScrollRestoration,
  useNavigation,
  useRouteLoaderData,
} from "react-router";
import "./app.css";

export function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        {/* Global fonts go in root.tsx, not layout components */}
        <link rel="preconnect" href="https://fonts.googleapis.com" />
        <link
          href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"
          rel="stylesheet"
        />
        <Meta />
        <Links />
      </head>
      <body>
        {children}
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

export default function App() {
  const navigation = useNavigation();
  const isNavigating = navigation.state !== "idle";

  return (
    <div className="app-layout">
      {/* Global loading indicator */}
      {isNavigating && <ProgressBar />}

      {/* Global navigation */}
      <header className="app-header">
        <nav className="app-nav">
          <NavLink
            to="/"
            className={({ isActive }) => (isActive ? "active" : "")}
          >
            Home
          </NavLink>
          <NavLink
            to="/products"
            className={({ isActive }) => (isActive ? "active" : "")}
          >
            Products
          </NavLink>
        </nav>
      </header>

      {/* Page content */}
      <main className="app-main">
        <Outlet />
      </main>

      {/* Global footer */}
      <footer className="app-footer">
        © {new Date().getFullYear()} My App
      </footer>
    </div>
  );
}

Note: Be defensive in your Layout component since it renders the ErrorBoundary. Use useRouteLoaderData("root") instead of useLoaderData to handle cases where the loader threw. See error-handling.md for error boundary patterns.


routes.ts (Required)

Defines the URL structure and maps URLs to route modules.

For detailed routing patterns, see routing.md.

Quick example:

import {
  type RouteConfig,
  route,
  index,
  layout,
} from "@react-router/dev/routes";

export default [
  index("./home.tsx"),
  route("about", "./about.tsx"),
  layout("./dashboard/layout.tsx", [
    route("dashboard", "./dashboard/index.tsx"),
    route("dashboard/settings", "./dashboard/settings.tsx"),
  ]),
] satisfies RouteConfig;

react-router.config.ts (Optional)

Configures framework-level settings:

import type { Config } from "@react-router/dev/config";

export default {
  // App directory (default: "app")
  appDirectory: "app",

  // Build output directory (default: "build")
  buildDirectory: "build",

  // Enable/disable SSR (default: true)
  ssr: true,

  // Pre-render routes at build time
  prerender: ["/", "/about", "/pricing"],

  // Base path for all routes
  basename: "/my-app",

  // Future flags
  future: {
    v8_middleware: true,
  },
} satisfies Config;

Common Configuration Options

Option Default Purpose
ssr true Enable server-side rendering
prerender [] Routes to pre-render as static HTML
basename "/" Base URL path for all routes
appDirectory "app" Source directory
buildDirectory "build" Build output directory
future {} Enable future flags

SPA Mode

Disable SSR for a single-page application:

export default {
  ssr: false,
} satisfies Config;

Pre-rendering

Pre-render routes to static HTML at build time:

export default {
  async prerender({ getStaticPaths }) {
    const dynamicPaths = await getStaticPaths();
    return ["/", "/about", ...dynamicPaths];
  },
} satisfies Config;

entry.client.tsx (Optional)

The browser entry point for hydrating server-rendered markup. React Router provides a default, but you can customize it:

npx react-router reveal
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import { HydratedRouter } from "react-router/dom";

startTransition(() => {
  hydrateRoot(
    document,
    <StrictMode>
      <HydratedRouter />
    </StrictMode>,
  );
});

Use cases for customizing:

  • Initialize client-side libraries (analytics, error tracking)
  • Add client-only providers
  • Custom hydration logic

entry.server.tsx (Optional)

The server entry point for rendering HTML responses. React Router provides a default for Node, but you may need to customize for other runtimes (Cloudflare, Deno).

npx react-router reveal
import { PassThrough } from "node:stream";
import type { EntryContext } from "react-router";
import { createReadableStreamFromReadable } from "@react-router/node";
import { ServerRouter } from "react-router";
import { renderToPipeableStream } from "react-dom/server";

export default function handleRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  routerContext: EntryContext,
) {
  return new Promise((resolve, reject) => {
    const { pipe, abort } = renderToPipeableStream(
      <ServerRouter context={routerContext} url={request.url} />,
      {
        onShellReady() {
          responseHeaders.set("Content-Type", "text/html");
          const body = new PassThrough();
          const stream = createReadableStreamFromReadable(body);
          resolve(
            new Response(stream, {
              headers: responseHeaders,
              status: responseStatusCode,
            }),
          );
          pipe(body);
        },
        onShellError(error: unknown) {
          reject(error);
        },
      },
    );
  });
}

// Optional: Control streaming timeout
export const streamTimeout = 10000;

// Optional: Handle errors
export function handleError(error: unknown, { request }: { request: Request }) {
  if (!request.signal.aborted) {
    console.error(error);
  }
}

.server Modules

Files with .server in the name (e.g., auth.server.ts) are server-only and excluded from client bundles.

app/
├── utils/
│   ├── db.server.ts      # Server-only
│   ├── auth.server.ts    # Server-only
│   └── format.ts         # Shared

Use for:

  • Database connections
  • Authentication utilities
  • Environment variables/secrets
  • Server-only APIs
// db.server.ts
import { PrismaClient } from "@prisma/client";

export const db = new PrismaClient();

Safety: The build will fail if .server code accidentally ends up in the client bundle.

Important: Route modules should NOT use .server - they have special handling.


.client Modules

Files with .client in the name (e.g., analytics.client.ts) are client-only and excluded from server bundles.

app/
├── utils/
│   ├── analytics.client.ts  # Client-only
│   ├── browser.client.ts    # Client-only
│   └── format.ts            # Shared

Use for:

  • Browser-specific APIs (localStorage, navigator)
  • Client-only libraries (charting, animations)
  • Feature detection
// analytics.client.ts
export function trackEvent(name: string) {
  window.gtag?.("event", name);
}

Note: Values exported from .client modules are undefined on the server. Only use them in useEffect or event handlers.


Where to Place Stylesheets and Fonts

Global styles and fonts belong in root.tsx, not in layout components:

  • Global fonts: In root.tsx <head> (see example above)
  • Route-specific stylesheets: Use the links export in route modules (see route-modules.md)

Anti-Patterns

Don't create a separate layout just for nav/footer - put global UI in root.tsx.

Don't use flat routes when nesting makes sense - see routing.md for proper nested route patterns.


See Also

Source: SKILL.md on GitHub

No alerts16d5 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    This skill is a comprehensive documentation reference for building applications with React Router's framework mode. It provides standard architectural patterns, code examples, and best practices for routing, data loading, actions, and session management. No security risks or malicious patterns were detected.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer7mo

    2/13 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 220978e. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub last week.

Steadyupdated 8 months ago

README badge

README badge for remix-run/agent-skills/react-router-framework-mode