Why Remix Needs Specific AI Rules
Remix is built on web fundamentals: HTTP requests and responses, HTML forms, cookies, and the standard Request/Response API. Every Remix pattern maps directly to a web platform concept. AI assistants don't understand this philosophy — they generate React SPA patterns (useEffect for data fetching, fetch for mutations, client-side state for form data) that bypass Remix's architecture entirely.
The result is Remix code that works but misses every benefit the framework provides: automatic data revalidation after mutations, progressive enhancement without JavaScript, nested route data loading, and race condition prevention. The AI writes a React app that happens to run on Remix, not a Remix app.
These rules target Remix v2+ (which merged into React Router v7). The core patterns — loaders, actions, nested routes — are the same in both. Specify your version.
Rule 1: Loaders for All Data Loading
The rule: 'Use loader functions for all data fetching. Loaders run on the server, have access to the database and environment variables, and return data via json(). Components access loader data with useLoaderData(). Never use useEffect + fetch for initial data loading — it causes waterfall requests, layout shift, and missed SEO. Loaders run in parallel for nested routes.'
For typed loaders: 'Export a loader function from every route that needs data: export async function loader({ request, params }: LoaderFunctionArgs) { ... return json(data); }. Use useLoaderData<typeof loader>() for type-safe data access in the component. TypeScript infers the return type automatically.'
For conditional data: 'Check authentication in the loader: if the user isn't authenticated, redirect("/login"). Return different data based on URL search params: const url = new URL(request.url); const page = url.searchParams.get("page"). Loaders are the single source of data for the route — everything the component needs comes from the loader.'
- Loaders for all data — never useEffect + fetch for initial loads
- json() for return, useLoaderData<typeof loader>() for typed access
- redirect() for auth checks — return different data based on URL params
- Loaders run in parallel for nested routes — no waterfall
- Server-only: database access, env vars, cookies — never exposed to client
AI generates useEffect + fetch for initial data. In Remix, this creates waterfalls, layout shift, and missed SEO. Loaders run on the server, in parallel for nested routes, before any HTML is sent. Always use loaders.
Rule 2: Actions for All Mutations
The rule: 'Use action functions for all data mutations (create, update, delete). Actions handle form submissions: export async function action({ request }: ActionFunctionArgs) { const formData = await request.formData(); ... }. After an action completes, Remix automatically revalidates all loaders on the page — your UI updates without any client-side state management.'
For forms: 'Use Remix's <Form> component for all forms — not HTML <form>. <Form method="post"> submits to the route's action. Use hidden fields for action identification: <input type="hidden" name="_action" value="delete" />. For multiple actions per route, dispatch on the _action field in the action function.'
For optimistic UI: 'Use useNavigation() to detect pending form submissions. Show optimistic state while the action is processing: const navigation = useNavigation(); const isSubmitting = navigation.state === "submitting". Use useFetcher() for mutations that shouldn't trigger navigation (like/unlike, add to cart).'
After an action completes, Remix automatically revalidates all loaders on the page. Your UI updates without any client-side state management, cache invalidation, or manual refetching. This is Remix's superpower.
Rule 3: Nested Routes and Layouts
The rule: 'Use Remix's nested routing for layout composition. Parent routes render shared UI (navigation, sidebar) and an <Outlet /> for child content. Each route segment loads its own data independently. Use route modules in app/routes/ following Remix's file naming: _index.tsx (index), $id.tsx (dynamic), _layout.tsx (pathless layout). Never create layout components manually — use route nesting.'
For route organization: 'Group related routes with route folders: routes/dashboard/ contains _layout.tsx, _index.tsx, settings.tsx, profile.tsx. The layout wraps all children. Use pathless layouts (prefixed with _) for shared UI without adding URL segments. Use route groups with parentheses for organization without nesting.'
For data inheritance: 'Child routes can access parent loader data with useRouteLoaderData("route-id"). Use this for data that's already loaded by a parent (user session, navigation items) — don't re-fetch in children. Set route IDs in the route module for stable references.'
Rule 4: Progressive Enhancement
The rule: 'All Remix forms work without JavaScript. <Form method="post"> submits as a standard HTML form when JS is disabled. This is progressive enhancement — the base functionality works without JS, and JS adds enhanced UX (optimistic updates, pending UI, client-side validation). Never break this contract: every form must work with JS disabled.'
For testing: 'Test every form with JavaScript disabled (disable JS in browser devtools). The form should submit, the action should process, and the page should show the result. If it doesn't work without JS, you've broken progressive enhancement — the form relies on client-side logic that the server can't handle.'
For client-side enhancement: 'Use useNavigation for pending/submitting states. Use useFetcher for non-navigation mutations. Use useActionData for action return values (validation errors). These hooks enhance the experience when JS is available — they don't replace server-side functionality.'
- All forms work without JS — standard HTML form submission
- JS adds: optimistic UI, pending states, client-side validation
- Test with JS disabled — if it breaks, you've bypassed Remix's model
- useNavigation for pending — useFetcher for inline mutations — useActionData for errors
- Never prevent default on form submit — let Remix handle it
Disable JavaScript in devtools and submit every form. If it works, you have progressive enhancement. If it breaks, you've bypassed Remix's model. This is the ultimate quality test for Remix code.
Rule 5: Error Boundaries and Catch Boundaries
The rule: 'Export an ErrorBoundary from every route that could throw errors. ErrorBoundary catches both thrown errors and loader/action failures. Use isRouteErrorResponse(error) to distinguish between expected errors (404, 403) and unexpected errors (server crash). Display user-friendly messages — never expose stack traces. Nested error boundaries catch errors without crashing the parent layout.'
For expected errors: 'Throw Response objects for expected errors: throw json({ message: "Not found" }, { status: 404 }). In ErrorBoundary, check isRouteErrorResponse(error) and display status-appropriate messages. Use throw redirect() for auth failures — not an error boundary.'
For unexpected errors: 'Unexpected errors (database down, unhandled exception) surface in ErrorBoundary as Error objects. Log the full error server-side. Display a generic "Something went wrong" message to the user. Include a retry button or link to go back. Never show implementation details in error messages.'
Complete Remix Rules Template
Consolidated rules for Remix v2+ projects.
- Loaders for data — never useEffect + fetch — useLoaderData<typeof loader>()
- Actions for mutations — <Form method='post'> — automatic revalidation after action
- Nested routes for layouts — <Outlet /> for children — useRouteLoaderData for parent data
- Progressive enhancement: all forms work without JS — test with JS disabled
- useNavigation for pending — useFetcher for inline mutations — useActionData for errors
- ErrorBoundary on every route — isRouteErrorResponse for expected vs unexpected
- redirect() for auth — json() for data — throw Response for expected errors
- Vitest + Testing Library — MSW for API mocking — Playwright for e2e