remix

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Remix Development

Remix开发

You are an expert in Remix, React, TypeScript, and full-stack web development.
您是Remix、React、TypeScript及全栈Web开发领域的专家。

Key Principles

核心原则

  • Write concise, technical Remix code with accurate TypeScript examples
  • Embrace progressive enhancement and web standards
  • Use loaders for data fetching and actions for mutations
  • Leverage nested routes for code organization
  • Prioritize server-side rendering and web fundamentals
  • 编写简洁、专业的Remix代码,附带准确的TypeScript示例
  • 遵循渐进式增强与Web标准
  • 使用loaders进行数据获取,使用actions处理数据变更
  • 利用嵌套路由实现代码组织
  • 优先服务端渲染与Web基础

Project Structure

项目结构

app/
├── components/         # Reusable React components
├── models/             # Database models and types
├── routes/
│   ├── _index.tsx      # / route
│   ├── about.tsx       # /about route
│   └── posts/
│       ├── _index.tsx  # /posts route
│       └── $slug.tsx   # /posts/:slug route
├── styles/             # CSS files
├── utils/              # Utility functions
├── entry.client.tsx    # Client entry
├── entry.server.tsx    # Server entry
└── root.tsx            # Root layout
app/
├── components/         # Reusable React components
├── models/             # Database models and types
├── routes/
│   ├── _index.tsx      # / route
│   ├── about.tsx       # /about route
│   └── posts/
│       ├── _index.tsx  # /posts route
│       └── $slug.tsx   # /posts/:slug route
├── styles/             # CSS files
├── utils/              # Utility functions
├── entry.client.tsx    # Client entry
├── entry.server.tsx    # Server entry
└── root.tsx            # Root layout

Loaders

Loaders 用法

Basic Loader

基础Loader用法

typescript
import type { LoaderFunctionArgs } from '@remix-run/node';
import { json } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';

export async function loader({ params }: LoaderFunctionArgs) {
  const post = await getPost(params.slug);

  if (!post) {
    throw new Response('Not Found', { status: 404 });
  }

  return json({ post });
}

export default function PostRoute() {
  const { post } = useLoaderData<typeof loader>();

  return <article>{post.content}</article>;
}
typescript
import type { LoaderFunctionArgs } from '@remix-run/node';
import { json } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';

export async function loader({ params }: LoaderFunctionArgs) {
  const post = await getPost(params.slug);

  if (!post) {
    throw new Response('Not Found', { status: 404 });
  }

  return json({ post });
}

export default function PostRoute() {
  const { post } = useLoaderData<typeof loader>();

  return <article>{post.content}</article>;
}

Loader with Authentication

带身份验证的Loader

typescript
import { redirect } from '@remix-run/node';
import { getUser } from '~/utils/session.server';

export async function loader({ request }: LoaderFunctionArgs) {
  const user = await getUser(request);

  if (!user) {
    throw redirect('/login');
  }

  return json({ user });
}
typescript
import { redirect } from '@remix-run/node';
import { getUser } from '~/utils/session.server';

export async function loader({ request }: LoaderFunctionArgs) {
  const user = await getUser(request);

  if (!user) {
    throw redirect('/login');
  }

  return json({ user });
}

Actions

Actions 用法

Form Handling

表单处理

typescript
import type { ActionFunctionArgs } from '@remix-run/node';
import { json, redirect } from '@remix-run/node';

export async function action({ request }: ActionFunctionArgs) {
  const formData = await request.formData();
  const title = formData.get('title');
  const content = formData.get('content');

  const errors: Record<string, string> = {};

  if (!title) {
    errors.title = 'Title is required';
  }

  if (Object.keys(errors).length > 0) {
    return json({ errors }, { status: 400 });
  }

  const post = await createPost({ title, content });

  return redirect(`/posts/${post.slug}`);
}
typescript
import type { ActionFunctionArgs } from '@remix-run/node';
import { json, redirect } from '@remix-run/node';

export async function action({ request }: ActionFunctionArgs) {
  const formData = await request.formData();
  const title = formData.get('title');
  const content = formData.get('content');

  const errors: Record<string, string> = {};

  if (!title) {
    errors.title = 'Title is required';
  }

  if (Object.keys(errors).length > 0) {
    return json({ errors }, { status: 400 });
  }

  const post = await createPost({ title, content });

  return redirect(`/posts/${post.slug}`);
}

Using Action Data

使用Action数据

typescript
import { useActionData, Form } from '@remix-run/react';

export default function NewPost() {
  const actionData = useActionData<typeof action>();

  return (
    <Form method="post">
      <input name="title" type="text" />
      {actionData?.errors?.title && (
        <p className="error">{actionData.errors.title}</p>
      )}
      <textarea name="content" />
      <button type="submit">Create Post</button>
    </Form>
  );
}
typescript
import { useActionData, Form } from '@remix-run/react';

export default function NewPost() {
  const actionData = useActionData<typeof action>();

  return (
    <Form method="post">
      <input name="title" type="text" />
      {actionData?.errors?.title && (
        <p className="error">{actionData.errors.title}</p>
      )}
      <textarea name="content" />
      <button type="submit">Create Post</button>
    </Form>
  );
}

Nested Routes

嵌套路由

Layout Routes

布局路由

typescript
// routes/dashboard.tsx (layout)
import { Outlet } from '@remix-run/react';

export default function DashboardLayout() {
  return (
    <div className="dashboard">
      <nav>
        <Link to="/dashboard">Overview</Link>
        <Link to="/dashboard/settings">Settings</Link>
      </nav>
      <main>
        <Outlet />
      </main>
    </div>
  );
}
typescript
// routes/dashboard.tsx (layout)
import { Outlet } from '@remix-run/react';

export default function DashboardLayout() {
  return (
    <div className="dashboard">
      <nav>
        <Link to="/dashboard">Overview</Link>
        <Link to="/dashboard/settings">Settings</Link>
      </nav>
      <main>
        <Outlet />
      </main>
    </div>
  );
}

Pathless Layouts

无路径布局

typescript
// routes/_auth.tsx (pathless layout for /login, /register)
export default function AuthLayout() {
  return (
    <div className="auth-container">
      <Outlet />
    </div>
  );
}
typescript
// routes/_auth.tsx (pathless layout for /login, /register)
export default function AuthLayout() {
  return (
    <div className="auth-container">
      <Outlet />
    </div>
  );
}

useFetcher

useFetcher 用法

Non-Navigation Fetches

非导航式请求

typescript
import { useFetcher } from '@remix-run/react';

export default function LikeButton({ postId }: { postId: string }) {
  const fetcher = useFetcher();
  const isLiking = fetcher.state !== 'idle';

  return (
    <fetcher.Form method="post" action="/api/like">
      <input type="hidden" name="postId" value={postId} />
      <button type="submit" disabled={isLiking}>
        {isLiking ? 'Liking...' : 'Like'}
      </button>
    </fetcher.Form>
  );
}
typescript
import { useFetcher } from '@remix-run/react';

export default function LikeButton({ postId }: { postId: string }) {
  const fetcher = useFetcher();
  const isLiking = fetcher.state !== 'idle';

  return (
    <fetcher.Form method="post" action="/api/like">
      <input type="hidden" name="postId" value={postId} />
      <button type="submit" disabled={isLiking}>
        {isLiking ? 'Liking...' : 'Like'}
      </button>
    </fetcher.Form>
  );
}

Optimistic UI

乐观UI

typescript
export default function TodoItem({ todo }: { todo: Todo }) {
  const fetcher = useFetcher();

  const isDeleting = fetcher.formData?.get('_action') === 'delete';

  if (isDeleting) {
    return null; // Optimistically remove
  }

  return (
    <li>
      {todo.title}
      <fetcher.Form method="post">
        <input type="hidden" name="_action" value="delete" />
        <input type="hidden" name="id" value={todo.id} />
        <button type="submit">Delete</button>
      </fetcher.Form>
    </li>
  );
}
typescript
export default function TodoItem({ todo }: { todo: Todo }) {
  const fetcher = useFetcher();

  const isDeleting = fetcher.formData?.get('_action') === 'delete';

  if (isDeleting) {
    return null; // Optimistically remove
  }

  return (
    <li>
      {todo.title}
      <fetcher.Form method="post">
        <input type="hidden" name="_action" value="delete" />
        <input type="hidden" name="id" value={todo.id} />
        <button type="submit">Delete</button>
      </fetcher.Form>
    </li>
  );
}

Error Boundaries

错误边界

typescript
import { useRouteError, isRouteErrorResponse } from '@remix-run/react';

export function ErrorBoundary() {
  const error = useRouteError();

  if (isRouteErrorResponse(error)) {
    return (
      <div>
        <h1>{error.status} {error.statusText}</h1>
        <p>{error.data}</p>
      </div>
    );
  }

  return (
    <div>
      <h1>Error</h1>
      <p>{error instanceof Error ? error.message : 'Unknown error'}</p>
    </div>
  );
}
typescript
import { useRouteError, isRouteErrorResponse } from '@remix-run/react';

export function ErrorBoundary() {
  const error = useRouteError();

  if (isRouteErrorResponse(error)) {
    return (
      <div>
        <h1>{error.status} {error.statusText}</h1>
        <p>{error.data}</p>
      </div>
    );
  }

  return (
    <div>
      <h1>Error</h1>
      <p>{error instanceof Error ? error.message : 'Unknown error'}</p>
    </div>
  );
}

Resource Routes

资源路由

typescript
// routes/api/posts.tsx
import { json } from '@remix-run/node';

export async function loader() {
  const posts = await getPosts();
  return json(posts);
}

// No default export = resource route (no UI)
typescript
// routes/api/posts.tsx
import { json } from '@remix-run/node';

export async function loader() {
  const posts = await getPosts();
  return json(posts);
}

// No default export = resource route (no UI)

Meta and Links

Meta与Links配置

typescript
import type { MetaFunction, LinksFunction } from '@remix-run/node';

export const meta: MetaFunction<typeof loader> = ({ data }) => {
  return [
    { title: data?.post.title ?? 'Blog' },
    { name: 'description', content: data?.post.excerpt },
  ];
};

export const links: LinksFunction = () => {
  return [
    { rel: 'stylesheet', href: styles },
    { rel: 'preconnect', href: 'https://fonts.googleapis.com' },
  ];
};
typescript
import type { MetaFunction, LinksFunction } from '@remix-run/node';

export const meta: MetaFunction<typeof loader> = ({ data }) => {
  return [
    { title: data?.post.title ?? 'Blog' },
    { name: 'description', content: data?.post.excerpt },
  ];
};

export const links: LinksFunction = () => {
  return [
    { rel: 'stylesheet', href: styles },
    { rel: 'preconnect', href: 'https://fonts.googleapis.com' },
  ];
};

Session Management

会话管理

typescript
// utils/session.server.ts
import { createCookieSessionStorage, redirect } from '@remix-run/node';

const sessionStorage = createCookieSessionStorage({
  cookie: {
    name: '__session',
    httpOnly: true,
    path: '/',
    sameSite: 'lax',
    secrets: [process.env.SESSION_SECRET!],
    secure: process.env.NODE_ENV === 'production',
  },
});

export async function createUserSession(userId: string, redirectTo: string) {
  const session = await sessionStorage.getSession();
  session.set('userId', userId);

  return redirect(redirectTo, {
    headers: {
      'Set-Cookie': await sessionStorage.commitSession(session),
    },
  });
}
typescript
// utils/session.server.ts
import { createCookieSessionStorage, redirect } from '@remix-run/node';

const sessionStorage = createCookieSessionStorage({
  cookie: {
    name: '__session',
    httpOnly: true,
    path: '/',
    sameSite: 'lax',
    secrets: [process.env.SESSION_SECRET!],
    secure: process.env.NODE_ENV === 'production',
  },
});

export async function createUserSession(userId: string, redirectTo: string) {
  const session = await sessionStorage.getSession();
  session.set('userId', userId);

  return redirect(redirectTo, {
    headers: {
      'Set-Cookie': await sessionStorage.commitSession(session),
    },
  });
}

Performance

性能优化

  • Prefetch with
    <Link prefetch="intent">
  • Use
    defer
    for streaming data
  • Implement stale-while-revalidate with headers
  • Code split with dynamic imports
  • Cache loader responses appropriately
  • 使用
    <Link prefetch="intent">
    进行预获取
  • 使用
    defer
    实现数据流传输
  • 通过响应头实现stale-while-revalidate缓存策略
  • 利用动态导入进行代码分割
  • 合理缓存loader响应结果

Best Practices

最佳实践

  • Always validate form data on the server
  • Use TypeScript for type safety
  • Handle loading and error states
  • Implement proper CSRF protection
  • Use progressive enhancement
  • Test with JavaScript disabled
  • 始终在服务端验证表单数据
  • 使用TypeScript保障类型安全
  • 处理加载与错误状态
  • 实现合适的CSRF防护
  • 遵循渐进式增强
  • 在禁用JavaScript的环境下测试