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
// ✅ 推荐:带严格单一返回值的类型化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
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
// ✅ 推荐:描述性、声明式的数据修改
<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): . 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. 控制器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
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通过<Suspense>支持慢速数据部分