remixjs-best-practices

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Remix Best Practices (2025-2026 Edition)

Remix最佳实践(2025-2026版)

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.
本指南介绍了使用Remix构建可扩展、高性能应用的现代化最佳实践,特别聚焦于向React Router v7的迁移,以及为Remix v3做好面向未来的适配。

🚀 Key Trends (2025+)

🚀 核心趋势(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.
  • React Router v7 就是 Remix: 所有Remix功能现在都已集成到React Router v7中。新项目应该直接使用React Router v7启动。
  • 服务优先思维模式: Loaders和Actions在服务端运行。
  • 采用「未来标志」: 始终在
    remix.config.js
    vite.config.ts
    中启用v7未来标志,确保迁移过程顺畅。
  • Codemod迁移工具: 使用
    npx codemod remix/2/react-router/upgrade
    迁移现有v2版本应用。

🏗️ Architecture & Data Loading

🏗️ 架构与数据加载

1. Server-First Data Flow

1. 服务优先数据流

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>;
}
除非绝对必要,否则避免客户端请求(
useEffect
)。
  • Loaders: 服务端获取数据。
  • Actions: 服务端修改数据。
  • 组件: 仅渲染loader返回的内容。
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

2. 优先使用Form Actions而非
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>
使用HTML表单(或Remix
<Form>
)处理数据修改。这种方式在无JS环境下也能运行,并且能自动处理竞态条件。
tsx
// ✅ Good: Descriptive, declarative mutation
<Form method="post" action="/update-profile">
  <button type="submit">Save</button>
</Form>

3. Progressive Enhancement

3. 渐进式增强

Design features to work without JavaScript first. Remix handles the "hydration" to make it interactive (SPA feel) automatically.
首先设计无JavaScript也能运行的功能。Remix会自动处理「hydration(注水)」,让功能具备交互性(SPA体验)。

🛡️ Error Handling Patterns

🛡️ 错误处理模式

1. Granular Error Boundaries

1. 细粒度错误边界

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>;
}
不要仅依赖根ErrorBoundary。在嵌套路由中放置边界,避免局部故障导致整个页面崩溃。
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

2. 预期错误与非预期错误

  • Expected (404, 401):
    throw new Response(...)
    . Caught by specific logic or boundaries.
  • Unexpected (500): Let the app crash to the nearest ErrorBoundary.
  • 预期错误(404、401): 使用
    throw new Response(...)
    抛出,由特定逻辑或边界捕获。
  • 非预期错误(500): 让应用向上冒泡到最近的ErrorBoundary处理。

3. Controller Actions (Validation)

3. 控制器Actions(校验)

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>}
从actions返回错误,而不是抛出错误。这样可以保留用户已输入的内容。
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

1.
Cache-Control

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" }
  });
};
Loaders可以输出缓存头,对公共数据使用缓存头。
typescript
export const loader = async () => {
  return json(data, {
    headers: { "Cache-Control": "public, max-age=3600" }
  });
};

2. Streaming (Defer)

2. 流式响应(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
对加载较慢的数据(比如第三方API)使用
defer
,避免阻塞初始HTML渲染。
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

📚 参考资料