remixjs-best-practices
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseRemix 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 or
remix.config.jsto ensuring smooth migration.vite.config.ts - Codemod Migration: Use to migrate existing v2 apps.
npx codemod remix/2/react-router/upgrade
- React Router v7 就是 Remix: 所有Remix功能现在都已集成到React Router v7中。新项目应该直接使用React Router v7启动。
- 服务优先思维模式: Loaders和Actions仅在服务端运行。
- 采用「未来标志」: 始终在或
remix.config.js中启用v7未来标志,确保迁移过程顺畅。vite.config.ts - Codemod迁移工具: 使用迁移现有v2版本应用。
npx codemod remix/2/react-router/upgrade
🏗️ Architecture & Data Loading
🏗️ 架构与数据加载
1. Server-First Data Flow
1. 服务优先数据流
Avoid client-side fetching () unless absolutely necessary.
useEffect- 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
onClick2. 优先使用Form Actions而非onClick
onClickUse HTML Forms (or Remix ) for mutations. This works without JS and handles race conditions automatically.
<Form>tsx
// ✅ Good: Descriptive, declarative mutation
<Form method="post" action="/update-profile">
<button type="submit">Save</button>
</Form>使用HTML表单(或Remix )处理数据修改。这种方式在无JS环境下也能运行,并且能自动处理竞态条件。
<Form>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): . Caught by specific logic or boundaries.
throw new Response(...) - 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
Cache-Control1. Cache-Control
头
Cache-ControlLoaders 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 for slow data (e.g., third-party APIs) to unblock the initial HTML render.
defertypescript
export const loader = async () => {
const critical = await getCriticalData();
const slow = getSlowData(); // Promise
return defer({ critical, slow });
};
// UI supports <Suspense> for the slow part对加载较慢的数据(比如第三方API)使用,避免阻塞初始HTML渲染。
defertypescript
export const loader = async () => {
const critical = await getCriticalData();
const slow = getSlowData(); // Promise
return defer({ critical, slow });
};
// UI supports <Suspense> for the slow part