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.

referencespending-ui.md

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

Pending UI and Optimistic Updates

Key principle: Show expected results immediately using fetcher.formData, then let React Router sync with the server.

Quick Reference

Pattern Hook Use Case
Optimistic mutations useFetcher + formData Likes, ratings, toggles
Global loading indicator useNavigation Page-level spinner
Link pending state NavLink Nav item loading indicator
Deferred data Await + Suspense Stream slow data

For mutation patterns (when to use Form vs useFetcher), see actions.md.


useNavigation

Track global navigation state:

import { useNavigation } from "react-router";

export default function Root() {
  const navigation = useNavigation();
  const isNavigating = Boolean(navigation.location);

  return (
    <html>
      <body className={isNavigating ? "loading" : ""}>
        {isNavigating && <GlobalSpinner />}
        <Outlet />
      </body>
    </html>
  );
}

Navigation States

  • navigation.state - "idle", "loading", or "submitting"
  • navigation.location - The location being navigated to (if any)
  • navigation.formData - Form data being submitted (if any)
  • navigation.formMethod - HTTP method of the submission
const navigation = useNavigation();

// Check if navigating
const isNavigating = navigation.state !== "idle";

// Check if submitting a form
const isSubmitting = navigation.state === "submitting";

// Check if loading after submission
const isLoading = navigation.state === "loading";

NavLink Pending State

Show pending state on the specific link being clicked:

import { NavLink } from "react-router";

function Nav() {
  return (
    <NavLink to="/dashboard">
      {({ isPending }) => <span>Dashboard {isPending && <Spinner />}</span>}
    </NavLink>
  );
}

Or use className:

<NavLink
  to="/dashboard"
  className={({ isPending }) => (isPending ? "pending" : "")}
>
  Dashboard
</NavLink>

useFetcher for Local State

Each fetcher tracks its own state independently:

import { useFetcher } from "react-router";

function LikeButton({ postId, liked }) {
  const fetcher = useFetcher();

  // Show pending state while submitting
  const isPending = fetcher.state !== "idle";

  return (
    <fetcher.Form method="post" action={`/posts/${postId}/like`}>
      <button disabled={isPending}>
        {isPending ? "..." : liked ? "Unlike" : "Like"}
      </button>
    </fetcher.Form>
  );
}

Fetcher States

  • fetcher.state - "idle", "submitting", or "loading"
  • fetcher.data - Data returned from the action/loader
  • fetcher.formData - Form data being submitted

Optimistic UI with useFetcher (Recommended Pattern)

This is the standard pattern for mutations. Show the expected result immediately using fetcher.formData:

import { useFetcher } from "react-router";

function LikeButton({ postId, initialLiked }) {
  const fetcher = useFetcher();

  // Optimistic: check pending form data first, fallback to server state
  const liked = fetcher.formData
    ? fetcher.formData.get("liked") === "true"
    : initialLiked;

  return (
    <fetcher.Form method="post" action={`/posts/${postId}/like`}>
      <input type="hidden" name="liked" value={String(!liked)} />
      <button>{liked ? "❤️" : "🤍"}</button>
    </fetcher.Form>
  );
}

Complete Optimistic UI Example

import { useFetcher } from "react-router";

function RatingStars({ itemId, currentRating }) {
  const fetcher = useFetcher();

  // 1. Check if we're submitting - use the pending value
  // 2. Otherwise use the server value
  const displayRating = fetcher.formData
    ? Number(fetcher.formData.get("rating"))
    : currentRating;

  const isSubmitting = fetcher.state !== "idle";

  return (
    <fetcher.Form method="post" action={`/items/${itemId}/rate`}>
      <div style={{ opacity: isSubmitting ? 0.5 : 1 }}>
        {[1, 2, 3, 4, 5].map((star) => (
          <button key={star} type="submit" name="rating" value={star}>
            {star <= displayRating ? "★" : "☆"}
          </button>
        ))}
      </div>
    </fetcher.Form>
  );
}

Why fetcher.formData?

  • Instant feedback - UI updates immediately on click
  • No loading spinners needed - the optimistic state IS the loading state
  • Automatic rollback - if the action fails, loaders revalidate and reset to server state

Optimistic UI with useNavigation

For form submissions that navigate:

import { Form, useNavigation } from "react-router";

function NewProjectForm() {
  const navigation = useNavigation();

  // Get optimistic value from submission
  const optimisticTitle = navigation.formData?.get("title");
  const isSubmitting = navigation.state === "submitting";

  return (
    <Form method="post">
      <input type="text" name="title" />
      <button disabled={isSubmitting}>
        {isSubmitting ? "Creating..." : "Create"}
      </button>

      {optimisticTitle && <p>Creating "{optimisticTitle}"...</p>}
    </Form>
  );
}

Skeleton Loading

Show skeletons while data loads:

import { Suspense } from "react";
import { Await } from "react-router";

export async function loader() {
  return {
    fastData: await getFastData(),
    slowData: getSlowData(), // Don't await
  };
}

export default function Page({ loaderData }: Route.ComponentProps) {
  return (
    <div>
      <h1>{loaderData.fastData.title}</h1>

      <Suspense fallback={<CommentsSkeleton />}>
        <Await resolve={loaderData.slowData}>
          {(data) => <Comments data={data} />}
        </Await>
      </Suspense>
    </div>
  );
}

Disabling During Submission

Prevent double submissions:

function ContactForm() {
  const navigation = useNavigation();
  const isSubmitting = navigation.state === "submitting";

  return (
    <Form method="post">
      <input type="text" name="message" disabled={isSubmitting} />
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? "Sending..." : "Send"}
      </button>
    </Form>
  );
}

Busy Indicators with CSS

Use CSS for simple loading indicators:

.loading {
  opacity: 0.5;
  pointer-events: none;
}
<body className={isNavigating ? "loading" : ""}>

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