All skills
sergiodxa avatar

/frontend-react-router-best-practices

@9da9532

React Router performance and architecture patterns. Use when writing loaders, actions, forms, routes, or working with React Router data fetching. Triggers on tasks involving React Router routes, data loading, form handling, or route organization.

Use this Skill: https://skilld.dev/gh/sergiodxa/agent-skills/frontend-react-router-best-practices

This session only. Nothing lands on disk.

rulesloader-request-caching.md

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

Request-Level Caching in Loaders

API clients automatically deduplicate calls within the same request. Fetch the same data in multiple loaders without worrying about duplicate network requests.

Why

  • Nested routes often need the same data (user, permissions, etc.)
  • React Router runs all loaders in parallel for a single request
  • API clients use request-scoped caching to dedupe identical calls
  • No manual coordination needed between loaders

How It Works

When you call the same API function with the same parameters in multiple loaders during the same request, the API client:

  1. Makes the network request on the first call
  2. Caches the result for the duration of the request
  3. Returns the cached result for subsequent identical calls
  4. Clears the cache after the request completes
// routes/dashboard.tsx (parent)
export async function loader({ request, context }: Route.LoaderArgs) {
  let client = await authenticate(request, { context });
  let user = await getUser(client); // First call - makes network request
  return data({ user });
}

// routes/dashboard.settings.tsx (child)
export async function loader({ request, context }: Route.LoaderArgs) {
  let client = await authenticate(request, { context });
  let user = await getUser(client); // Same call - returns cached result
  let settings = await getSettings(client, user.id);
  return data({ user, settings });
}

Both loaders run in parallel. The context contains the batcher (context.batcher), so authenticate and subsequent API calls share the same request-scoped cache. The second getUser(client) call returns instantly from the cache.

When to Fetch vs Use useRouteLoaderData

Fetch in loader when:

  • The loader needs the data for its own logic (not just UI)
  • You need to transform or combine the data with other data
  • The child route can be accessed directly (not always through parent)
// Child loader needs user.id to fetch settings
export async function loader({ request, context }: Route.LoaderArgs) {
  let client = await authenticate(request, { context });
  let user = await getUser(client); // Need user.id for next call
  let settings = await getSettings(client, user.id);
  return data({ settings });
}

Use useRouteLoaderData when:

  • You only need the data for rendering (no loader logic)
  • The parent route always loads before this route
  • You're certain the data exists in the parent
// Child only needs user for display
export default function Component() {
  const { user } =
    useRouteLoaderData<typeof dashboardLoader>("routes/dashboard");
  return <WelcomeMessage name={user.name} />;
}

How It Works Internally

Use the remix-utils batcher middleware to get a request-scoped batcher:

// app/middleware/batcher.server.ts
import { createBatcherMiddleware } from "remix-utils/middleware/batcher";

export const [batcherMiddleware, getBatcher] = createBatcherMiddleware();
// app/routes/dashboard/route.tsx
import { batcherMiddleware, getBatcher } from "~/middleware/batcher.server";

export const middleware: Route.MiddlewareFunction[] = [batcherMiddleware];

export async function loader({ context }: Route.LoaderArgs) {
  let batcher = getBatcher(context);
  let result = await batcher.batch("key", async () => {
    return await getData();
  });
  return data({ result });
}

Your API client can use the batcher for GET requests automatically:

// clients/api.ts
override async get<Type>(path: string, body?: ParamsInput, init?: RequestInit) {
  let url = new URL(path, this.baseURL);
  url.search = snakeCaseSearchParams(body).toString();

  // Batch by [pathname, search] - same URL = same cached result
  return this.batch([url.pathname, url.search], () =>
    this.collectTiming("api", `${url.pathname}${url.search}`, async () => {
      let response = await super.get(url.href, { ...init });
      return await response.text().then(parse<Type>);
    })
  );
}

The batcher deduplicates based on the key array [pathname, search]:

  • Same pathname + search params = returns cached result
  • Different pathname or params = makes new request
  • Only GET requests are batched (POST/PUT/DELETE always execute)

Rules

  1. Always pass context to api() or authenticate() - enables request-level caching
  2. Don't avoid fetching data in nested loaders for "performance" - caching handles it
  3. Fetch data in each loader that needs it for logic, not just rendering
  4. Use useRouteLoaderData only for UI-only access to parent data
  5. Trust the API client's request-level caching - identical GET requests are deduped automatically

Source: SKILL.md on GitHub

2 warnings16d5 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    The skill provides a comprehensive set of developer guidelines and best practices for building React Router applications. It emphasizes performance, type safety, and security measures like input validation (Zod), safe redirects, and CSRF protection (Sec-Fetch). No malicious code, obfuscation, or data exfiltration patterns were detected.

  • Socket16d

    1 alert: gptSecurity

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer7mo

    56/56 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

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

Last checked against GitHub 2 months ago.

Dormantupdated 8 months ago

README badge

README badge for sergiodxa/agent-skills/frontend-react-router-best-practices