Skip to main content
rule

remix — Cursor rule: Remix app conventions

A Cursor rule that sets Remix project conventions: route logic next to UI, mutations through Form, heavy fetching in loader. Useful when the agent scatters code across files.

Install into Cursor · Manually: put the files in place All methods and platforms ↓
.cursor/rules/remix.mdc
Open on GitHub

What remix does

The rule covers how Remix sits on top of React Router v7 and insists that route logic stay beside its component. Every route gets its own folder holding route.tsx, loader.ts, action.ts, plus meta.ts or error-boundary.tsx when needed. Instead of client fetches and prop drilling, call the server function straight from loader.

Then come layout conventions: app/components, app/features, app/hooks, app/services, app/utils. Components and types use PascalCase, file names use kebab-case. Mutations go through Remix Form so built-in revalidation keeps working. Endpoints with no UI become resource routes.

A separate section handles performance and traps. Heavy data work belongs in loader and action, since those run on the server and keep the client bundle small. Slow data streams in through defer and Await wrapped in Suspense. Touching window, localStorage or document inside loader or action fails outright: those functions execute on the server, so pass a token through cookies, headers or form data instead. The rule ships as .cursor/rules/remix.mdc and applies to **/*.{js,jsx,ts,tsx}.

What it helps with

  • split a Remix route into a folder with route.tsx, loader.ts and action.ts
  • move a mutation off client fetch onto Remix Form with revalidation
  • lift a heavy request out of useEffect into a server loader
  • stream slow data with defer and Await instead of blocking first render
  • catch window or localStorage access inside server code

Example prompts for the agent

  • Split the posts.$postId route into a folder: route.tsx, loader.ts, error-boundary.tsx
  • Rewrite the new-post form with Remix Form, an action, and a redirect to /posts
  • Move dashboard data loading into loader and stream the slow part with defer

How to install remix

Manually: put the files in place

.cursor/rules/remix.mdc

Frontmatter: description, globs, alwaysApply

Step by step, with commands for each platform: Cursor rules: .mdc files, four modes and what replaced .cursorrules

Questions about remix

How do I install the remix rule in Cursor?

Create .cursor/rules/remix.mdc in your project and paste the rule content into it. It then applies to files matching **/*.{js,jsx,ts,tsx}.

What does the remix rule do?

It gives the agent Remix conventions: co-locate route logic with UI, use Form for mutations, keep data work in server loader and action, and defer slow data with defer.

Why can't I use window inside a loader?

loader and action run on the server, where window, localStorage and document do not exist. Pass what you need through cookies, headers or form data.

Source text

remix.mdc · CC0-1.0
---
description: This guide outlines definitive best practices for building robust, performant, and maintainable applications with Remix, emphasizing co-location, type safety, and efficient data handling.
globs: **/*.{js,jsx,ts,tsx}
---
# Remix Best Practices

Remix provides a powerful full-stack framework built on React Router v7. Adhering to these guidelines ensures a consistent, high-quality codebase that leverages Remix's strengths for server-rendered applications.

## 1. Code Organization and Structure

**Always co-locate route logic and UI.** Each route must live in its own folder, containing its `route.tsx` component, `loader.ts`, `action.ts`, and optional `meta.ts` or `error-boundary.tsx`. This keeps related concerns together and simplifies navigation.

❌ BAD:
```tsx
// app/data/posts.ts
export async function getPost(id: string) { /* ... */ }

// app/components/PostDetail.tsx
import { getPost } from '~/data/posts';
function PostDetail() {
  // Client-side fetch or prop drilling
}

// app/routes/posts.$postId.tsx
import PostDetail from '~/components/PostDetail';
export default function PostRoute() {
  return <PostDetail />;
}
```

✅ GOOD:
```tsx
// app/routes/posts.$postId/route.tsx
import { json, useLoaderData } from "@remix-run/node";
import type { LoaderFunctionArgs } from "@remix-run/node";

export async function loader({ params }: LoaderFunctionArgs) {
  const post = await db.getPost(params.postId); // Direct server call
  if (!post) throw new Response("Not Found", { status: 404 });
  return json({ post });
}

export default function PostDetailRoute() {
  const { post } = useLoaderData<typeof loader>();
  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </div>
  );
}

// app/routes/posts.$postId/error-boundary.tsx
import { isRouteErrorResponse, useRouteError } from "@remix-run/react";
export function ErrorBoundary() {
  const error = useRouteError();
  if (isRouteErrorResponse(error)) {
    return <div>Error: {error.status} {error.statusText}</div>;
  }
  return <div>Unknown Error!</div>;
}
```

**Maintain a clear `src` directory hierarchy.** Separate concerns into logical folders like `app/components`, `app/features`, `app/hooks`, `app/utils`, `app/services`, etc.

❌ BAD:
```
app/
├── index.tsx
├── post-list.tsx
├── user-profile.tsx
├── api.ts
└── helpers.ts
```

✅ GOOD:
```
app/
├── components/       // Reusable UI components
├── features/         // Domain-specific components/logic
├── hooks/            // Custom React hooks
├── routes/           // File-based routes (as above)
├── services/         // Database/API interaction logic
├── utils/            // Generic utility functions
└── entry.client.tsx
└── entry.server.tsx
```

**Enforce consistent naming conventions.** Use PascalCase for React components and types, and kebab-case for file names.

❌ BAD: `mycomponent.jsx`, `MyComponent.ts`, `userProfile.tsx`
✅ GOOD: `MyComponent.tsx`, `user-profile.ts`, `UserProfile.tsx`

## 2. Common Patterns and Anti-patterns

**Use Remix `Form` for all data mutations.** This leverages Remix's built-in revalidation and pending UI states for a robust user experience.

❌ BAD:
```tsx
import { useState } from 'react';
function NewPostForm() {
  const [title, setTitle] = useState('');
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    await fetch('/posts', { method: 'POST', body: JSON.stringify({ title }) });
  };
  return <form onSubmit={handleSubmit}>...</form>;
}
```

✅ GOOD:
```tsx
// app/routes/posts.new/route.tsx
import { Form, redirect } from "@remix-run/react";
import type { ActionFunctionArgs } from "@remix-run/node";

export async function action({ request }: ActionFunctionArgs) {
  const formData = await request.formData();
  const title = formData.get("title");
  await db.createPost({ title });
  return redirect("/posts");
}

export default function NewPostForm() {
  return (
    <Form method="post">
      <input type="text" name="title" />
      <button type="submit">Create Post</button>
    </Form>
  );
}
```

**Utilize Resource Routes for API-only endpoints.** For data fetching or mutations not tied to a specific UI page, create a resource route.

❌ BAD: Creating a separate API server or `app/api/posts.ts` that isn't a route.
✅ GOOD:
```tsx
// app/routes/api.posts.$postId.ts
import { json } from "@remix-run/node";
import type { LoaderFunctionArgs } from "@remix-run/node";

export async function loader({ params }: LoaderFunctionArgs) {
  const post = await db.getPost(params.postId);
  return json(post);
}
```

## 3. Performance Considerations

**Perform all heavy data fetching in `loader` and `action` functions.** These run exclusively on the server, keeping your client bundle small and fast.

❌ BAD:
```tsx
// app/routes/dashboard.tsx
import { useEffect, useState } from 'react';
function Dashboard() {
  const [data, setData] = useState(null);
  useEffect(() => {
    fetch('/api/dashboard-data').then(res => res.json()).then(setData);
  }, []);
  return <div>{data ? 'Loaded' : 'Loading...'}</div>;
}
```

✅ GOOD:
```tsx
// app/routes/dashboard.tsx
import { json, useLoaderData } from "@remix-run/node";
export async function loader() {
  const data = await db.getDashboardData(); // Server-side data fetch
  return json(data);
}
export default function Dashboard() {
  const data = useLoaderData<typeof loader>();
  return <div>{data ? 'Loaded' : 'Loading...'}</div>;
}
```

**Defer non-critical data with `defer` and `Await` for faster initial page loads.** This allows critical data to render immediately while slower data streams in.

❌ BAD: Waiting for all data to resolve before rendering.
```tsx
// loader that waits for everything
export async function loader() {
  const critical = await getCriticalData();
  const slow = await getSlowData(); // Blocks initial render
  return json({ critical, slow });
}
```

✅ GOOD:
```tsx
// loader that streams slow data
import { defer, Await, json } from "@remix-run/node";
import { Suspense } from "react";

export async function loader() {
  const critical = await getCriticalData();
  const slowPromise = getSlowData(); // Returns a promise
  return defer({ critical, slow: slowPromise });
}

export default function MyRoute() {
  const { critical, slow } = useLoaderData<typeof loader>();
  return (
    <div>
      <h1>Critical: {critical.message}</h1>
      <Suspense fallback={<p>Loading slow data...</p>}>
        <Await resolve={slow}>
          {(resolvedSlow) => <p>Slow: {resolvedSlow.message}</p>}
        </Await>
      </Suspense>
    </div>
  );
}
```

## 4. Common Pitfalls and Gotchas

**Avoid client-side specific APIs in `loader` or `action` functions.** These functions run on the server, so `window`, `localStorage`, `document`, etc., are unavailable.

❌ BAD:
```tsx
export async function loader() {
  const token = window.localStorage.getItem('token'); // Will fail on server
  // ...
}
```

✅ GOOD: Pass necessary data via cookies, headers, or form data.
```tsx
export async function loader({ request }: LoaderFunctionArgs) {
  const cookieHeader = request.headers.get("Cookie");
  const token = getAuthTokenFromCookie(cookieHeader); // Server-safe
  // ...
}
```

**Treat `useLoaderData` and `useActionData` as immutable.** Remix revalidates data, so direct mutations won't persist and can lead to unexpected behavior.

❌ BAD:
```tsx
const { items } = useLoaderData<typeof loader>();
items.push("new item"); // Don't mutate loader data directly
```

✅ GOOD: Create a new array or object if you need to modify data client-side.
```tsx
const { items } = useLoaderData<typeof loader>();
const [clientItems, setClientItems] = useState(items);
// ... setClientItems([...clientItems, "new item"])
```

## 5. Hooks Best Practices

**Use `useNavigation` for pending UI states.** This provides a seamless user experience during transitions.

❌ BAD: Manually managing `isLoading` state with `useState` for form submissions.
✅ GOOD:
```tsx
import { Form, useNavigation } from "@remix-run/react";
export default function MyForm() {
  const navigation = useNavigation();
  const isSubmitting = navigation.state === "submitting";
  return (
    <Form method="post">
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? "Saving..." : "Save"}
      </button>
    </Form>
  );
}
```

**Use `useFetcher` for non-navigation data mutations or fetches.** This is ideal for background updates, like liking a post, without triggering a full page navigation.

❌ BAD: Using `<Form>` for every small interaction that doesn't need a redirect.
✅ GOOD:
```tsx
import { useFetcher } from "@remix-run/react";
export default function LikeButton({ postId }: { postId: string }) {
  const fetcher = useFetcher();
  const isLiking = fetcher.state === "submitting";
  return (
    <fetcher.Form method="post" action="/api/like">
      <input type="hidden" name="postId" value={postId} />
      <button type="submit" disabled={isLiking}>
        {isLiking ? "Liking..." : "Like"}
      </button>
    </fetcher.Form>
  );
}
```

## 6. Type Safety with TypeScript

**Always use TypeScript with Remix.** Leverage `typeof loader` and `typeof action` for type-safe data access, ensuring robust code and improved developer experience.

❌ BAD:
```tsx
// No types, relies on runtime checks
function MyComponent() {
  const data = useLoaderData(); // data is 'any'
  // ...
}
```

✅ GOOD:
```tsx
import { json, useLoaderData } from "@remix-run/node";
export async function loader() {
  return json({ message: "Hello", count: 42 });
}
export default function MyComponent() {
  const data = useLoaderData<typeof loader>(); // data is { message: string; count: number; }
  return <p>{data.message} {data.count}</p>;
}
```

## 7. Security

**Implement rate-limiting for critical endpoints.** Protect your `action` and `resource` routes from abuse. This is typically done at the edge (Cloudflare, Vercel) or with server-side middleware.

❌ BAD: No protection for public-facing mutation endpoints.
✅ GOOD:
```ts
// app/routes/signup.ts
import { json, ActionFunctionArgs } from "@remix-run/node";
import { rateLimit } from "~/utils/rate-limiter.server"; // Custom server utility

export async function action({ request }: ActionFunctionArgs) {
  await rateLimit(request, "signup", { limit: 5, window: 60 }); // 5 requests per minute
  // ... signup logic
  return json({ success: true });
}
```

© sanjeed5 · CC0-1.0 · sanjeed5/awesome-cursor-rules-mdc

Similar IDE rules

All IDE rules for Frontend and UI →

From the same set87

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor