src/skills/web-meta-framework-remix/SKILL.md
File-based routing, loaders, actions, defer streaming, useFetcher, error boundaries, progressive enhancement
npx skillsauth add agents-inc/skills web-meta-framework-remixInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Quick Guide: Each route exports a
loaderfor reads and anactionfor writes. Both run on the server. Data flows through loaders, mutations go through actions, forms work without JavaScript, and nested routes enable parallel data loading.json()anddefer()are deprecated in React Router v7 -- return raw objects instead, usedata()for custom headers/status.
<migration_notice>
Remix has merged into React Router v7. What was planned as Remix v3 is now React Router v7 "framework mode".
| Remix v2 (Deprecated) | React Router v7 (Current) |
| --------------------------------- | ------------------------------------------------ |
| json(data) | Return raw objects directly |
| json(data, { status, headers }) | data(data, { status, headers }) |
| defer({ key: promise }) | Return { key: promise } with Single Fetch |
| @remix-run/node imports | react-router / @react-router/node |
| LoaderFunctionArgs | Route.LoaderArgs (generated types) |
| ActionFunctionArgs | Route.ActionArgs (generated types) |
| useLoaderData<typeof loader>() | loaderData prop via Route.ComponentProps |
| RemixServer | ServerRouter (from react-router) |
| RemixBrowser | HydratedRouter (from react-router/dom) |
| File-based routing (automatic) | routes.ts + optional @react-router/fs-routes |
This skill covers both Remix v2 and React Router v7 patterns. Examples use Remix v2 imports by default with RR v7 equivalents documented in examples/react-router-v7.md.
Migration guide: Upgrading from Remix
</migration_notice>
<critical_requirements>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST export loaders and actions as named exports from route modules only -- they do not work in non-route files)
(You MUST throw Response objects for expected errors (404, 403) -- use ErrorBoundary for handling)
(You MUST await critical data and return non-critical data as Promises for streaming)
(You MUST use named constants for HTTP status codes -- no magic numbers)
</critical_requirements>
Auto-detection: Remix routes, React Router v7, loader function, action function, clientAction, clientLoader, useLoaderData, useActionData, useFetcher, defer, ErrorBoundary, Form component, meta function, links function, Single Fetch, ServerRouter, HydratedRouter, Route.LoaderArgs, Route.ComponentProps, shouldRevalidate
When to use:
When NOT to use:
Key patterns covered:
Remix simplifies full-stack development to a single mental model: each route exports a loader for reads and an action for writes. Both functions execute exclusively on the server, enabling direct database access without exposing secrets to the client.
Core Principles:
Data Flow:
URL Change -> Loader(s) Execute -> Component Renders -> User Interacts
|
Action Executes -> Loaders Revalidate
</philosophy>
Files in app/routes/ become URL paths. File naming conventions control nesting, layouts, and dynamic segments.
| File Name | URL | Description |
| ----------------- | ------------- | ----------------------------- |
| _index.tsx | / | Index route (root) |
| about.tsx | /about | Static route |
| blog.$slug.tsx | /blog/:slug | Dynamic parameter |
| blog_.tsx | /blog | Pathless layout escape |
| _auth.tsx | (none) | Layout route (no URL segment) |
| _auth.login.tsx | /login | Route nested in layout |
| $.tsx | /* | Splat/catch-all route |
// app/routes/blog.$slug.tsx -- dynamic route with loader
import type { LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
const HTTP_NOT_FOUND = 404;
export async function loader({ params }: LoaderFunctionArgs) {
const post = await db.post.findUnique({ where: { slug: params.slug } });
if (!post) throw new Response("Not Found", { status: HTTP_NOT_FOUND });
return { post };
}
Why good: File names map directly to URLs, $ prefix for dynamic segments, loader params are typed, named constant for status code
See examples/core.md for complete route examples and examples/nested-routes.md for layout nesting patterns.
Loaders are server-only functions that provide data to routes. They run on initial server render and on client navigation via fetch.
const HTTP_NOT_FOUND = 404;
export async function loader({ params, request }: LoaderFunctionArgs) {
const user = await db.user.findUnique({ where: { id: params.userId } });
if (!user) {
throw json({ message: "User not found" }, { status: HTTP_NOT_FOUND });
}
return json({ user });
}
Key rules:
useLoaderData<typeof loader>() for type-safe access (or Route.ComponentProps in RR v7)shouldRevalidate to optimize unnecessary re-runsSee examples/loaders.md for authentication, pagination, and caching examples.
Actions handle non-GET requests (POST, PUT, DELETE, PATCH). They run before loaders and enable progressive form handling.
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const intent = formData.get("intent");
switch (intent) {
case "update": {
/* ... */ return json({ success: true });
}
case "delete": {
/* ... */ return redirect("/items");
}
default:
throw new Error(`Unknown intent: ${intent}`);
}
}
Key rules:
intent field for multiple actions in one routejson({ errors }, { status: 400 })See examples/actions.md for validation and examples/forms.md for multi-form patterns.
Await critical data, return Promises for non-critical data that can stream in.
// Remix v2: use defer()
return defer({
user, // Awaited -- critical
analytics: getAnalytics(), // Promise -- streams in
});
// React Router v7: return raw objects with Promises
return {
user, // Awaited -- critical
analytics: getAnalytics(), // Promise -- streams via Single Fetch
};
Render streamed data with <Suspense> + <Await>:
<Suspense fallback={<Skeleton />}>
<Await resolve={analytics} errorElement={<p>Failed to load</p>}>
{(data) => <Chart data={data} />}
</Await>
</Suspense>
When to stream: Analytics, comments, recommendations, secondary content below the fold. When NOT to stream: Auth state, page title, SEO-critical content, data for page structure.
See examples/deferred.md for complete streaming examples.
useFetcher enables data loading and mutations without page navigation. Essential for inline interactions.
const fetcher = useFetcher();
// Optimistic UI: show expected state immediately
const optimisticIsLiked = fetcher.formData
? fetcher.formData.get("liked") === "true"
: isLiked;
Use <Form> for: Create/login/wizards -- actions that should change the URL.
Use useFetcher for: Like buttons, toggles, inline editing, search autocomplete.
See examples/optimistic.md for optimistic UI and debounced search.
Export ErrorBoundary from route modules. Distinguish between thrown Response errors and unexpected JavaScript errors.
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
// Thrown Response: render status-specific UI
return <div role="alert"><h1>{error.status}</h1></div>;
}
if (error instanceof Error) {
// Unexpected error: generic fallback
return <div role="alert"><h1>Unexpected Error</h1></div>;
}
return <div role="alert"><h1>Unknown Error</h1></div>;
}
Key rules:
json({ message }, { status: 404 }) for expected errorsisRouteErrorResponse() checks if error was a thrown ResponseSee examples/error-handling.md for multi-status error boundaries.
Export meta for SEO metadata and links for stylesheets/preloads.
export const meta: MetaFunction<typeof loader> = ({ data }) => {
if (!data) return [{ title: "Not Found" }];
return [
{ title: `${data.post.title} | ${SITE_NAME}` },
{ property: "og:title", content: data.post.title },
{ tagName: "link", rel: "canonical", href: url },
];
};
Gotcha: meta function receives null data on error -- always handle the missing data case.
Gotcha: links function cannot access loader data -- use meta with tagName: "link" for dynamic links.
See examples/meta.md for Open Graph and Twitter Card patterns.
Routes without a default export become resource routes -- useful for APIs, webhooks, and file downloads.
// app/routes/api.health.ts (no default export = resource route)
export async function loader() {
return json({ status: "healthy", timestamp: new Date().toISOString() });
}
See examples/resource-routes.md for webhook and file download examples.
Nested routes share parent layouts and load data in parallel. Parent loaders provide shared data, child loaders run concurrently.
| Pattern | Purpose |
| --------------------- | ---------------------------------------------- |
| admin.tsx | Layout (has <Outlet />) |
| admin._index.tsx | Index route (renders at parent URL) |
| admin.users.tsx | Nested child route |
| admin_.settings.tsx | Escapes parent layout with trailing underscore |
| _auth.tsx | Pathless layout with leading underscore |
See examples/nested-routes.md for admin layout and pathless layout examples.
</patterns>Detailed Resources:
<red_flags>
High Priority Issues:
useLoaderData<typeof loader>() or Route.ComponentPropsMedium Priority Issues:
method="post" -- defaults to GET, action not calledGotchas & Edge Cases:
shouldRevalidate to optimize)defer() requires <Suspense> + <Await> wrapper -- forgetting causes errors?index query param for form actions targeting themmeta function receives null data on error -- must handle missing data caselinks function cannot access loader data -- use meta with tagName: "link" for dynamic linksclientAction takes priority when both action and clientAction exist -- server action is completely skipped</red_flags>
<critical_reminders>
All code must follow project conventions in CLAUDE.md
(You MUST export loaders and actions as named exports from route modules only -- they do not work in non-route files)
(You MUST throw Response objects for expected errors (404, 403) -- use ErrorBoundary for handling)
(You MUST await critical data and return non-critical data as Promises for streaming)
(You MUST use named constants for HTTP status codes -- no magic numbers)
Failure to follow these rules will break data loading, type safety, and error handling.
</critical_reminders>
development
Material Design component library for Vue 3
development
VitePress 1.x — Vue-powered static site generator for documentation sites, built on Vite
tools
Docusaurus 3.x documentation framework — site configuration, docs/blog plugins, sidebars, versioning, MDX, swizzling, and deployment
development
TanStack Form patterns - useForm, form.Field, validators, arrays, linked fields, createFormHook, type safety