home / skills / toilahuongg / shopify-agents-kit / remixjs-best-practices

remixjs-best-practices skill

/.claude/skills/remixjs-best-practices

This skill helps you adopt Remix best practices with React Router v7, server-first data loading, and robust error handling strategies.

npx playbooks add skill toilahuongg/shopify-agents-kit --skill remixjs-best-practices

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
3.8 KB
---
name: remixjs-best-practices
description: Best practices for Remix (2025-2026 Edition), focusing on React Router v7 migration, server-first data patterns, and error handling.
---

# Remix Best Practices (2025-2026 Edition)

This skill outlines modern best practices for building scalable, high-performance applications with Remix, specifically focusing on the transition to React Router v7 and future-proofing for Remix v3.

## 🚀 Key Trends (2025+)

*   **React Router v7 is Remix:** All Remix features are now part of React Router v7. New projects should start with React Router v7.
*   **Server-First Mental Model:** Loaders and Actions run *only* on the server.
*   **"Future Flags" Adoption:** Always enable v7 future flags in `remix.config.js` or `vite.config.ts` to ensuring smooth migration.
*   **Codemod Migration:** Use `npx codemod remix/2/react-router/upgrade` to migrate existing v2 apps.

## 🏗️ Architecture & Data Loading

### 1. Server-First Data Flow
Avoid client-side fetching (`useEffect`) unless absolutely necessary.
*   **Loaders:** Fetch data server-side.
*   **Actions:** Mutate data server-side.
*   **Components:** render *what* the loader provides.

```typescript
// ✅ Good: Typed loader with single strict return
export const loader = async ({ request }: LoaderFunctionArgs) => {
  const user = await getUser(request);
  if (!user) throw new Response("Unauthorized", { status: 401 });
  return json({ user });
};

// Component gets fully typed data
export default function Dashboard() {
  const { user } = useLoaderData<typeof loader>(); 
  return <h1>Hello, {user.name}</h1>;
}
```

### 2. Form Actions over `onClick`
Use HTML Forms (or Remix `<Form>`) for mutations. This works without JS and handles race conditions automatically.

```tsx
// ✅ Good: Descriptive, declarative mutation
<Form method="post" action="/update-profile">
  <button type="submit">Save</button>
</Form>
```

### 3. Progressive Enhancement
Design features to work without JavaScript first. Remix handles the "hydration" to make it interactive (SPA feel) automatically.

## 🛡️ Error Handling Patterns

### 1. Granular Error Boundaries
Do not rely solely on a root ErrorBoundary. Place boundaries in nested routes to prevent a partial failure from crashing the entire page.

```tsx
// routes/dashboard.tsx (Nested Route)
export function ErrorBoundary() {
  const error = useRouteError();
  return <div className="p-4 bg-red-50">Widget crashed: {error.message}</div>;
}
```

### 2. Expected vs. Unexpected Errors
*   **Expected (404, 401):** `throw new Response(...)`. Caught by specific logic or boundaries.
*   **Unexpected (500):** Let the app crash to the nearest ErrorBoundary.

### 3. Controller Actions (Validation)
Return errors from actions, don't throw them. This preserves user input.

```typescript
// Action
if (name.length < 3) {
  return json({ errors: { name: "Too short" } }, { status: 400 });
}

// Component
const actionData = useActionData<typeof action>();
{actionData?.errors?.name && <span>{actionData.errors.name}</span>}
```

## ⚡ Performance Optimization

### 1. `Cache-Control` Headers
Loaders can output cache headers. Use them for public data.
```typescript
export const loader = async () => {
  return json(data, {
    headers: { "Cache-Control": "public, max-age=3600" }
  });
};
```

### 2. Streaming (Defer)
Use `defer` for slow data (e.g., third-party APIs) to unblock the initial HTML render.
```typescript
export const loader = async () => {
  const critical = await getCriticalData();
  const slow = getSlowData(); // Promise
  return defer({ critical, slow });
};

// UI supports <Suspense> for the slow part
```

## 📚 References

*   [React Router v7 Migration Guide](https://reactrouter.com/v7/upgrading/remix)
*   [Remix "Future Flags" Documentation](https://remix.run/docs/en/main/start/future)
*   [Remix Routing Guide](https://remix.run/docs/en/main/discussion/routes)

Overview

This skill describes modern Remix best practices for 2025–2026, with a focus on the React Router v7 migration, server-first data patterns, and robust error handling. It explains how to structure loaders, actions, forms, and error boundaries to build fast, resilient applications that progressively enhance. The guidance is practical and forward-looking to make apps ready for Remix v3 and React Router v7.

How this skill works

The guidance inspects common application areas: data loading (loaders/defer), mutations (actions/HTML forms), error handling (granular boundaries and expected vs. unexpected errors), and performance headers (Cache-Control). It recommends adopting server-first patterns where loaders and actions run only on the server, enabling predictable typed data flows and minimal client-side fetching. Migration tips include enabling future flags and running the official codemod to move from Remix v2 to React Router v7.

When to use it

  • Starting a new Remix project in 2025+ — start with React Router v7.
  • Migrating an existing Remix v2 app to future-proof for Remix v3.
  • Building features that must work without JavaScript (progressive enhancement).
  • Handling form submissions, validation, and preserving user input on errors.
  • Serving public, cacheable resources or streaming slow backend data.

Best practices

  • Prefer server-side loaders and actions; avoid useEffect data fetching unless necessary.
  • Use HTML <form> (or Remix <Form>) for all mutations to handle race conditions and work without JS.
  • Enable v7 future flags in remix.config.js or vite.config.ts early to smooth migration.
  • Add nested ErrorBoundaries to contain failures instead of a single root boundary.
  • Return action validation errors (json with status) instead of throwing to preserve form state.
  • Use Cache-Control and defer for streaming slow data to optimize TTFB and interactivity.

Example use cases

  • Dashboard with typed loader data and server-side auth checks, rendering user info from useLoaderData.
  • Profile update via a <Form> that returns validation errors from the action and re-displays inputs.
  • Migrating a v2 app: run the codemod and flip future flags, then update route boundaries for v7.
  • Rendering critical content immediately and deferring slow third-party requests with defer and Suspense.
  • Serving public product lists with Cache-Control headers from loaders for CDN caching.

FAQ

Do I need client-side fetching at all?

Rarely. Favor loaders and actions; use client fetching only for highly interactive widgets or after progressive enhancement.

When should I throw vs return errors?

Throw Responses for expected HTTP errors (401/404) so boundaries can catch them. Return JSON with status for validation so the form state is preserved.