Remix Loader基础 #

一、Loader概述 #

Loader 是 Remix 中用于在服务端加载数据的函数。它在渲染页面之前执行,将数据传递给页面组件。

二、基本用法 #

2.1 定义Loader #

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

export async function loader({ params, request }: LoaderFunctionArgs) {
  const posts = await getPosts();
  return json({ posts });
}

export default function Posts() {
  const { posts } = useLoaderData<typeof loader>();
  
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

2.2 Loader参数 #

Loader 函数接收一个参数对象:

tsx
export async function loader({ 
  params,      // 路由参数
  request,     // Request对象
  context,     // 应用上下文
}: LoaderFunctionArgs) {
  // ...
}

2.3 返回响应 #

使用 json 返回 JSON 响应:

tsx
import { json } from "@remix-run/node";

export async function loader() {
  const data = await fetchData();
  return json({ data });
}

// 带状态码
export async function loader() {
  const data = await fetchData();
  return json({ data }, { status: 200 });
}

三、获取参数 #

3.1 路由参数 #

tsx
// 文件: app/routes/posts.$id.tsx

export async function loader({ params }: LoaderFunctionArgs) {
  const { id } = params;
  const post = await getPost(id);
  
  if (!post) {
    throw new Response("文章不存在", { status: 404 });
  }
  
  return json({ post });
}

3.2 查询参数 #

tsx
export async function loader({ request }: LoaderFunctionArgs) {
  const url = new URL(request.url);
  const query = url.searchParams.get("q");
  const page = url.searchParams.get("page") || "1";
  
  const results = await search(query, parseInt(page));
  return json({ results });
}

3.3 请求头 #

tsx
export async function loader({ request }: LoaderFunctionArgs) {
  const userAgent = request.headers.get("User-Agent");
  const cookie = request.headers.get("Cookie");
  const authorization = request.headers.get("Authorization");
  
  // ...
}

四、响应类型 #

4.1 JSON响应 #

tsx
import { json } from "@remix-run/node";

export async function loader() {
  return json({ 
    message: "Hello",
    data: { id: 1, name: "Test" }
  });
}

4.2 重定向 #

tsx
import { redirect } from "@remix-run/node";

export async function loader({ request }: LoaderFunctionArgs) {
  const user = await getUser(request);
  
  if (!user) {
    return redirect("/login");
  }
  
  return json({ user });
}

4.3 自定义响应 #

tsx
export async function loader() {
  return new Response("Not Found", {
    status: 404,
    headers: {
      "Content-Type": "text/plain",
    },
  });
}

4.4 文件下载 #

tsx
export async function loader() {
  const file = await getFile();
  
  return new Response(file, {
    headers: {
      "Content-Type": "application/pdf",
      "Content-Disposition": "attachment; filename=document.pdf",
    },
  });
}

五、错误处理 #

5.1 抛出错误 #

tsx
export async function loader({ params }: LoaderFunctionArgs) {
  const post = await getPost(params.id);
  
  if (!post) {
    throw new Response("文章不存在", { status: 404 });
  }
  
  return json({ post });
}

5.2 错误边界 #

tsx
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>出错了: {error.message}</div>;
}

5.3 自定义错误类 #

tsx
class NotFoundError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "NotFoundError";
  }
}

export async function loader({ params }: LoaderFunctionArgs) {
  const post = await getPost(params.id);
  
  if (!post) {
    throw new NotFoundError("文章不存在");
  }
  
  return json({ post });
}

export function ErrorBoundary() {
  const error = useRouteError();
  
  if (error instanceof NotFoundError) {
    return <div>找不到资源: {error.message}</div>;
  }
  
  return <div>服务器错误</div>;
}

六、类型安全 #

6.1 类型推断 #

tsx
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.id);
  return json({ post });
}

export default function Post() {
  // 自动推断类型
  const { post } = useLoaderData<typeof loader>();
  
  return <div>{post.title}</div>;
}

6.2 定义返回类型 #

tsx
interface LoaderData {
  post: Post;
  comments: Comment[];
}

export async function loader({ params }: LoaderFunctionArgs): Promise<Response> {
  const [post, comments] = await Promise.all([
    getPost(params.id),
    getComments(params.id),
  ]);
  
  return json<LoaderData>({ post, comments });
}

七、加载状态 #

7.1 使用useNavigation #

tsx
import { useNavigation, useLoaderData } from "@remix-run/react";

export default function Page() {
  const navigation = useNavigation();
  const data = useLoaderData<typeof loader>();
  
  const isLoading = navigation.state === "loading";
  
  return (
    <div>
      {isLoading && <div className="loading">加载中...</div>}
      <div>{data.content}</div>
    </div>
  );
}

7.2 使用useFetcher #

tsx
import { useFetcher } from "@remix-run/react";

export default function Page() {
  const fetcher = useFetcher<typeof loader>();
  
  return (
    <div>
      <button onClick={() => fetcher.load("/api/data")}>
        刷新数据
      </button>
      
      {fetcher.state === "loading" && <div>加载中...</div>}
      
      {fetcher.data && (
        <div>{fetcher.data.message}</div>
      )}
    </div>
  );
}

八、条件加载 #

8.1 用户认证 #

tsx
export async function loader({ request }: LoaderFunctionArgs) {
  const user = await getUser(request);
  
  if (!user) {
    throw redirect("/login");
  }
  
  const data = await getPrivateData(user.id);
  return json({ user, data });
}

8.2 权限检查 #

tsx
export async function loader({ request }: LoaderFunctionArgs) {
  const user = await requireUser(request);
  
  if (user.role !== "admin") {
    throw new Response("无权限", { status: 403 });
  }
  
  const data = await getAdminData();
  return json({ data });
}

九、数据缓存 #

9.1 缓存控制 #

tsx
export async function loader() {
  const data = await fetchData();
  
  return json(data, {
    headers: {
      "Cache-Control": "public, max-age=60",
    },
  });
}

9.2 使用Headers对象 #

tsx
export async function loader() {
  const data = await fetchData();
  
  return json(data, {
    headers: new Headers({
      "Cache-Control": "public, max-age=3600",
      "X-Custom-Header": "value",
    }),
  });
}

十、最佳实践 #

10.1 数据验证 #

tsx
import { z } from "zod";

const paramsSchema = z.object({
  id: z.string().uuid(),
});

export async function loader({ params }: LoaderFunctionArgs) {
  const result = paramsSchema.safeParse(params);
  
  if (!result.success) {
    throw new Response("无效参数", { status: 400 });
  }
  
  const post = await getPost(result.data.id);
  return json({ post });
}

10.2 并行请求 #

tsx
export async function loader({ params }: LoaderFunctionArgs) {
  const [post, comments, author] = await Promise.all([
    getPost(params.id),
    getComments(params.id),
    getAuthor(params.id),
  ]);
  
  return json({ post, comments, author });
}

10.3 错误处理封装 #

tsx
export async function loader({ params }: LoaderFunctionArgs) {
  try {
    const post = await getPost(params.id);
    return json({ post });
  } catch (error) {
    if (error instanceof NotFoundError) {
      throw new Response("文章不存在", { status: 404 });
    }
    throw error;
  }
}

十一、总结 #

本章我们学习了:

  1. Loader定义:使用 loader 函数加载数据
  2. 参数获取:params、request、context
  3. 响应类型:json、redirect、自定义响应
  4. 错误处理:抛出错误和错误边界
  5. 类型安全:使用 TypeScript 类型推断

核心要点:

  • Loader 在服务端执行,数据在渲染前准备好
  • 使用 json 返回 JSON 响应
  • 使用 throw 处理错误情况
  • 始终验证输入参数
最后更新:2026-03-28