loader

📼 Remix Single: コンポーネントへのデータのロードをご覧ください

各ルートは、レンダリング時にルートにデータを提供する loader 関数を定義できます。

import { json } from "@remix-run/node"; // または cloudflare/deno
 
export const loader = async () => {
  return json({ ok: true });
};

この関数はサーバーでのみ実行されます。最初のサーバーレンダリングでは、HTMLドキュメントにデータを提供します。ブラウザでのナビゲーションでは、Remixはブラウザからfetchを介して関数を呼び出します。

つまり、データベースと直接通信したり、サーバー専用のAPIシークレットを使用したりできます。UIのレンダリングに使用されないコードは、ブラウザバンドルから削除されます。

データベースORMのPrismaを例として使用します。

import { useLoaderData } from "@remix-run/react";
 
import { prisma } from "../db";
 
export async function loader() {
  return json(await prisma.user.findMany());
}
 
export default function Users() {
  const data = useLoaderData<typeof loader>();
  return (
    <ul>
      {data.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

prismaloaderでのみ使用されるため、強調表示された行が示すように、コンパイラによってブラウザバンドルから削除されます。

`loader`から返すものは、コンポーネントがレンダリングしなくてもクライアントに公開されることに注意してください。`loader`は、パブリックAPIエンドポイントと同じように注意して扱ってください。

型安全性

loader とコンポーネントに対して、ネットワーク越しに型安全性を実現するには、useLoaderData<typeof loader> を使用します。

import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
 
export async function loader() {
  return json({ name: "Ryan", date: new Date() });
}
 
export default function SomeRoute() {
  const data = useLoaderData<typeof loader>();
}
  • data.name は文字列であることがわかります。
  • data.date は、json に日付オブジェクトを渡したにもかかわらず、文字列であることがわかります。クライアント遷移のためにデータがフェッチされる際、値は JSON.stringify を使用してネットワーク越しにシリアライズされ、型はそのことを認識しています。

params

ルートパラメータは、ルートファイル名によって定義されます。セグメントが $invoiceId のように $ で始まる場合、そのセグメントの URL からの値が loader に渡されます。

app/routes/invoices.$invoiceId.tsx
// ユーザーが /invoices/123 にアクセスした場合
export async function loader({
  params,
}: LoaderFunctionArgs) {
  params.invoiceId; // "123"
}

パラメータは主に、ID でレコードを検索するのに役立ちます。

app/routes/invoices.$invoiceId.tsx
// ユーザーが /invoices/123 にアクセスした場合
export async function loader({
  params,
}: LoaderFunctionArgs) {
  const invoice = await fakeDb.getInvoice(params.invoiceId);
  if (!invoice) throw new Response("", { status: 404 });
  return json(invoice);
}

request

これはFetch Requestインスタンスです。そのすべてのプロパティについては、MDNドキュメントを参照してください。

loaderでの最も一般的なユースケースは、ヘッダー(クッキーなど)と、リクエストからのURL URLSearchParamsを読み取ることです。

export async function loader({
  request,
}: LoaderFunctionArgs) {
  // クッキーを読み取る
  const cookie = request.headers.get("Cookie");
 
  // `?q=`の検索パラメータを解析する
  const url = new URL(request.url);
  const query = url.searchParams.get("q");
}

context

これは、サーバーアダプターの getLoadContext() 関数に渡されるコンテキストです。アダプターのリクエスト/レスポンス API と Remix アプリの間のギャップを埋めるための方法です。

この API はエスケープハッチであり、必要になることはまれです

express アダプターを例に挙げると:

server.ts
const {
  createRequestHandler,
} = require("@remix-run/express");
 
app.all(
  "*",
  createRequestHandler({
    getLoadContext(req, res) {
      // これがローダーコンテキストになります
      return { expressUser: req.user };
    },
  })
);

そして、loader がそれにアクセスできます。

app/routes/some-route.tsx
export async function loader({
  context,
}: LoaderFunctionArgs) {
  const { expressUser } = context;
  // ...
}

レスポンスインスタンスを返す

loader から Fetch Response を返す必要があります。

export async function loader() {
  const users = await db.users.findMany();
  const body = JSON.stringify(users);
  return new Response(body, {
    headers: {
      "Content-Type": "application/json",
    },
  });
}

json ヘルパー を使用すると、これを簡略化できるため、自分で構築する必要はありません。ただし、これらの2つの例は実質的に同じです。

import { json } from "@remix-run/node"; // または cloudflare/deno
 
export const loader = async () => {
  const users = await fakeDb.users.findMany();
  return json(users);
};

jsonloader をよりクリーンにするために少しだけ作業をしていることがわかります。また、json ヘルパーを使用して、レスポンスにヘッダーやステータスコードを追加することもできます。

import { json } from "@remix-run/node"; // または cloudflare/deno
 
export const loader = async ({
  params,
}: LoaderFunctionArgs) => {
  const project = await fakeDb.project.findOne({
    where: { id: params.id },
  });
 
  if (!project) {
    return json("Project not found", { status: 404 });
  }
 
  return json(project);
};

こちらも参照してください。

ローダーでのレスポンスのスロー

レスポンスを返すだけでなく、loader から Response オブジェクトをスローすることもできます。これにより、コールスタックを中断し、次の2つのいずれかを行うことができます。

  • 別のURLにリダイレクトする
  • ErrorBoundary を介してコンテキストデータとともに代替UIを表示する

以下は、ローダーでのコード実行を停止し、代替UIを表示するためにレスポンスをスローするユーティリティ関数を作成する方法を示す完全な例です。

app/db.ts
import { json } from "@remix-run/node"; // または cloudflare/deno
 
export function getInvoice(id) {
  const invoice = db.invoice.find({ where: { id } });
  if (invoice === null) {
    throw json("Not Found", { status: 404 });
  }
  return invoice;
}
app/http.ts
import { redirect } from "@remix-run/node"; // または cloudflare/deno
 
import { getSession } from "./session";
 
export async function requireUserSession(request) {
  const session = await getSession(
    request.headers.get("cookie")
  );
  if (!session) {
    // `redirect` や `json` のようなヘルパーは `Response` オブジェクトを返すため、
    // これらをスローできます。`redirect` レスポンスは別のURLにリダイレクトし、
    // 他のレスポンスは `ErrorBoundary` でレンダリングされるUIをトリガーします。
    throw redirect("/login", 302);
  }
  return session.get("user");
}
app/routes/invoice.$invoiceId.tsx
import type { LoaderFunctionArgs } from "@remix-run/node"; // または cloudflare/deno
import { json } from "@remix-run/node"; // または cloudflare/deno
import {
  isRouteErrorResponse,
  useLoaderData,
  useRouteError,
} from "@remix-run/react";
 
import { getInvoice } from "~/db";
import { requireUserSession } from "~/http";
 
export const loader = async ({
  params,
  request,
}: LoaderFunctionArgs) => {
  const user = await requireUserSession(request);
  const invoice = getInvoice(params.invoiceId);
 
  if (!invoice.userIds.includes(user.id)) {
    throw json(
      { invoiceOwnerEmail: invoice.owner.email },
      { status: 401 }
    );
  }
 
  return json(invoice);
};
 
export default function InvoiceRoute() {
  const invoice = useLoaderData<typeof loader>();
  return <InvoiceView invoice={invoice} />;
}
 
export function ErrorBoundary() {
  const error = useRouteError();
 
  if (isRouteErrorResponse(error)) {
    switch (error.status) {
      case 401:
        return (
          <div>
            <p>この請求書へのアクセス権がありません。</p>
            <p>
              アクセス権を得るには {error.data.invoiceOwnerEmail}
              連絡してください
            </p>
          </div>
        );
      case 404:
        return <div>請求書が見つかりません!</div>;
    }
 
    return (
      <div>
        問題が発生しました: {error.status}{" "}
        {error.statusText}
      </div>
    );
  }
 
  return (
    <div>
      問題が発生しました:{" "}
      {error?.message || "不明なエラー"}
    </div>
  );
}