Skip to content
Back to skills

Remix

ASecurity

Remix full-stack React framework. Covers loaders, actions, nested routing, and progressive enhancement. Use when building Remix applications. USE WHEN: user mentions "Remix", asks about "loaders", "actions", "Remix forms", "nested routes in Remix", "progressive enhancement", "defer in Remix", "useFetcher", "Remix deployment" DO NOT USE FOR: React without Remix - use `frontend-react` instead; Next.js - use `nextjs-app-router` instead; Nuxt - use `nuxt3` instead; SvelteKit - use `sveltekit` i...

  • 31 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 8, 2026
ai-agentstypescriptgoreactvuenextjsnodetestingfrontendsecurityperformance

Works with

  • cli
  • mcp

Security analysis

A100/100

Pro scans all 4 files and shows the line behind each finding

Scanned September 8, 2026

npx -y skills add claude-dev-suite/claude-dev-suite --skill remix --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Remix?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Remix
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/claude-dev-suite-remix/badge)](https://www.skillsdirectory.com/skills/claude-dev-suite-remix)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: remix
description: |
  Remix full-stack React framework. Covers loaders, actions, nested
  routing, and progressive enhancement. Use when building Remix
  applications.

  USE WHEN: user mentions "Remix", asks about "loaders", "actions", "Remix forms", "nested routes in Remix", "progressive enhancement", "defer in Remix", "useFetcher", "Remix deployment"

  DO NOT USE FOR: React without Remix - use `frontend-react` instead; Next.js - use `nextjs-app-router` instead; Nuxt - use `nuxt3` instead; SvelteKit - use `sveltekit` instead
allowed-tools: Read, Grep, Glob, Write, Edit
---
# Remix Core Knowledge

> **Full Reference**: See [advanced.md](advanced.md) for WebSocket integration, Server-Sent Events, Socket.IO patterns, and room management.

> **Deep Knowledge**: Use `mcp__documentation__fetch_docs` with technology: `remix` for comprehensive documentation.

## Route Module

```tsx
import type { LoaderFunctionArgs, ActionFunctionArgs } from '@remix-run/node';
import { json } from '@remix-run/node';
import { useLoaderData, Form } from '@remix-run/react';

// Server: Load data
export async function loader({ params }: LoaderFunctionArgs) {
  const user = await db.users.find(params.id);
  if (!user) throw new Response('Not Found', { status: 404 });
  return json({ user });
}

// Server: Handle mutations
export async function action({ request }: ActionFunctionArgs) {
  const formData = await request.formData();
  await db.users.update({ name: formData.get('name') });
  return json({ success: true });
}

// Client: UI
export default function UserPage() {
  const { user } = useLoaderData<typeof loader>();
  return (
    <Form method="post">
      <input name="name" defaultValue={user.name} />
      <button type="submit">Save</button>
    </Form>
  );
}
```

## Key Concepts

| Concept | Purpose |
|---------|---------|
| `loader` | GET requests, data fetching |
| `action` | POST/PUT/DELETE mutations |
| `Form` | Progressive enhanced forms |
| `useLoaderData` | Access loader data |
| `useFetcher` | Non-navigation mutations |

## Nested Routes

```
routes/
├── _index.tsx        → /
├── users.tsx         → /users (layout)
├── users._index.tsx  → /users (index)
└── users.$id.tsx     → /users/:id
```

## Error Handling

```tsx
export function ErrorBoundary() {
  const error = useRouteError();
  return <div>Error: {error.message}</div>;
}
```

## Production Readiness

### Security

```typescript
// app/utils/csrf.server.ts
import { createCookie } from '@remix-run/node';

export const csrfCookie = createCookie('csrf', {
  httpOnly: true,
  secure: process.env.NODE_ENV === 'production',
  sameSite: 'strict',
});

// app/root.tsx - Security headers
export const headers: HeadersFunction = () => ({
  'X-Frame-Options': 'DENY',
  'X-Content-Type-Options': 'nosniff',
  'Referrer-Policy': 'strict-origin-when-cross-origin',
  'Content-Security-Policy': "default-src 'self'",
});

// Input validation
import { z } from 'zod';

const UserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(2).max(100),
});

export async function action({ request }: ActionFunctionArgs) {
  const formData = await request.formData();
  const result = UserSchema.safeParse(Object.fromEntries(formData));

  if (!result.success) {
    return json({ errors: result.error.flatten() }, { status: 400 });
  }

  // Process valid data
}
```

### Error Handling

```typescript
// app/root.tsx
export function ErrorBoundary() {
  const error = useRouteError();

  if (isRouteErrorResponse(error)) {
    return (
      <div>
        <h1>{error.status} {error.statusText}</h1>
        <p>{error.data}</p>
      </div>
    );
  }

  return (
    <div>
      <h1>Unexpected Error</h1>
      <p>{error instanceof Error ? error.message : 'Unknown error'}</p>
    </div>
  );
}

// Per-route error boundary
// app/routes/users.$id.tsx
export function ErrorBoundary() {
  const error = useRouteError();

  if (isRouteErrorResponse(error) && error.status === 404) {
    return <div>User not found</div>;
  }

  throw error; // Re-throw to parent boundary
}
```

### Performance

```typescript
// Streaming with defer
import { defer } from '@remix-run/node';
import { Await, useLoaderData } from '@remix-run/react';

export async function loader() {
  const criticalData = await getCriticalData();
  const slowData = getSlowData(); // Don't await

  return defer({
    criticalData,
    slowData,
  });
}

export default function Page() {
  const { criticalData, slowData } = useLoaderData<typeof loader>();

  return (
    <div>
      <div>{criticalData.name}</div>
      <Suspense fallback={<Skeleton />}>
        <Await resolve={slowData}>
          {(data) => <SlowSection data={data} />}
        </Await>
      </Suspense>
    </div>
  );
}

// Caching
export const headers: HeadersFunction = () => ({
  'Cache-Control': 'public, max-age=300, s-maxage=3600',
});
```

### Session Management

```typescript
// app/utils/session.server.ts
import { createCookieSessionStorage, redirect } from '@remix-run/node';

const sessionStorage = createCookieSessionStorage({
  cookie: {
    name: '__session',
    httpOnly: true,
    maxAge: 60 * 60 * 24 * 7, // 1 week
    path: '/',
    sameSite: 'lax',
    secrets: [process.env.SESSION_SECRET!],
    secure: process.env.NODE_ENV === 'production',
  },
});

export async function requireUser(request: Request) {
  const session = await sessionStorage.getSession(request.headers.get('Cookie'));
  const userId = session.get('userId');

  if (!userId) {
    throw redirect('/login');
  }

  return userId;
}
```

### Testing

```typescript
// app/routes/users.test.ts
import { createRemixStub } from '@remix-run/testing';
import { render, screen } from '@testing-library/react';
import UsersPage, { loader } from './users';

describe('Users Page', () => {
  it('renders users', async () => {
    const RemixStub = createRemixStub([
      {
        path: '/',
        Component: UsersPage,
        loader: () => ({ users: [{ id: '1', name: 'John' }] }),
      },
    ]);

    render(<RemixStub />);

    expect(await screen.findByText('John')).toBeInTheDocument();
  });
});

// E2E with Playwright
test('user flow', async ({ page }) => {
  await page.goto('/users');
  await page.click('button:has-text("Add User")');
  await page.fill('input[name="name"]', 'Jane');
  await page.click('button:has-text("Save")');
  await expect(page.locator('text=Jane')).toBeVisible();
});
```

### Monitoring Metrics

| Metric | Target |
|--------|--------|
| Time to First Byte | < 100ms |
| First Contentful Paint | < 1s |
| Cumulative Layout Shift | < 0.1 |
| Form submission time | < 500ms |

### Checklist

- [ ] Security headers in root
- [ ] CSRF protection for forms
- [ ] Input validation with Zod
- [ ] ErrorBoundary per route
- [ ] Streaming with defer
- [ ] Cache headers for static data
- [ ] Secure session cookies
- [ ] Rate limiting on actions
- [ ] Testing with @remix-run/testing
- [ ] Progressive enhancement verified

## When NOT to Use This Skill

This skill is for Remix (React meta-framework). DO NOT use for:

- **React without Remix**: Use `frontend-react` skill instead
- **Next.js (React meta-framework)**: Use `nextjs-app-router` skill instead
- **Nuxt (Vue meta-framework)**: Use `nuxt3` skill instead
- **SvelteKit**: Use `sveltekit` skill instead
- **Astro**: Use `astro` skill instead
- **React Router SPA**: Use `frontend-react` with React Router documentation

## Anti-Patterns

| Anti-Pattern | Why It's Wrong | Correct Approach |
|--------------|----------------|------------------|
| Fetching in useEffect | Client-side only, no SSR, waterfalls | Use loader function for data fetching |
| Not using Form component | No progressive enhancement | Use <Form> from @remix-run/react |
| Ignoring ErrorBoundary | Uncaught errors crash route | Export ErrorBoundary from each route |
| Putting business logic in component | Not server-side, harder to test | Move logic to loader/action functions |
| Not validating formData | Security risk, type errors | Validate with Zod in action function |
| Using fetch instead of fetcher | Full page reload on mutation | Use useFetcher for non-navigation mutations |
| No cache headers | Poor performance | Set Cache-Control headers in loader |
| Secrets in client code | Exposed to client, security risk | Keep secrets in loader/action only |

## Quick Troubleshooting

| Issue | Possible Cause | Solution |
|-------|----------------|----------|
| "useLoaderData returns undefined" | No loader exported | Export loader function from route |
| Form submission doesn't work | Using <form> instead of <Form> | Import and use <Form> from @remix-run/react |
| Data not updating after action | Not revalidating | Remix auto-revalidates; check action return value |
| "Cannot access request.formData" | Wrong function signature | Use ActionFunctionArgs, await request.formData() |
| 404 on nested route | Incorrect file naming | Check routes/ file structure and naming conventions |
| Session not persisting | Cookie not returned | Return session.commit() in Set-Cookie header |
| Hydration mismatch | Server/client data differs | Ensure consistent data from loader |
| "ErrorBoundary is not a function" | Wrong export syntax | Export as: export function ErrorBoundary() {} |

## Reference Documentation
- [Route Conventions](quick-ref/route-conventions.md)
- [Forms & Actions](quick-ref/forms.md)

Files in this skill

  • SKILL.md9.2 KB
  • advanced.md7.1 KB
  • quick-ref/forms.md4 KB
  • quick-ref/route-conventions.md3.1 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…