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
// ✅ 推荐:带严格单一返回值的类型化loader
export const loader = async ({ request }: LoaderFunctionArgs) => {
  const user = await getUser(request);
  if (!user) throw new Response("Unauthorized", { status: 401 });
  return json({ user });
};

// 组件获取完全类型化的数据
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
// ✅ 推荐:描述性、声明式的数据修改
<Form method="post" action="/update-profile">
  <button type="submit">保存</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会自动处理“水合”过程,使其具备交互式(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(嵌套路由)
export function ErrorBoundary() {
  const error = useRouteError();
  return <div className="p-4 bg-red-50">组件崩溃:{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. 控制器Action(验证)

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>}
从Action返回错误,而非抛出错误。这样可以保留用户输入内容。
typescript
// Action
if (name.length < 3) {
  return json({ errors: { name: "过短" } }, { status: 400 });
}

// 组件
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通过<Suspense>支持慢速数据部分

📚 References

📚 参考资料