Remix@フレームワーク¶
はじめに¶
本サイトにつきまして、以下をご認識のほど宜しくお願いいたします。
01. Remixとは¶
Reactのreact-routerを拡張したフレームワークである。
Remixの90%はreact-routerとのこと。
ラッパーを減らし、ブラウザがデフォルトで提供する関数やオブジェクト、HTML要素をそのまま使用できるようになっている。
export async function action({request}) {
// formData関数はブラウザからデフォルトで提供されている
const formData = await request.formData();
}
<form method="post"></form>
02. Remixの仕組み¶
処理の流れ¶
▼ 概要¶
SSRのアプリケーションで以下の順に処理を実行し、データの取得からレンダリング(サーバーレンダリング/クライアントレンダリング)までを実施する。
ローダーの段階である程度加工された状態でバックエンドからデータを習得できれば、フロントエンドの処理時にかかる負荷をバックエンド側に寄せられる。
| 順番 | 名前 | ディレクトリ | ロジック | 責務 |
|---|---|---|---|---|
| 1 | ブラウザ上 | - | - | 最初、ブラウザからRemixにリクエストを送信する。ほかに、アクション中の remix-link コンポーネントが、URLクエリストリングのページ値(?page=n)が書き換える。 |
| 2 | ローダー | app/routes |
バックエンド | レンダリング前にバックエンドでデータを処理し、return json() でフロントエンドにデータを渡す。ローダーの段階でデータを用意しておき、フロントエンドではデータ表示ロジックだけを実装すると、ブラウザの描画のパフォーマンスが上がる。 |
| 3 | remixコンポーネント | app/components |
UIロジック、CSSスタイリングロジック、状態管理ロジック | レンダリング処理を実行する。reactコンポーネントとは区別する。また、app/components にあるreactコンポーネント(UIレンダリングロジック、状態管理ロジック)を呼び出す。 生成したページをブラウザに返信する。 |
| 4 | ブラウザ上 | - | - | Remixからページを取得し、表示する |
| 5 | アクション | app/routes |
バックエンド | レンダリング後のブラウザ操作に応じて、デザインパターンのコントローラーのようにクエリストリングやリクエストコンテキストを受信し、DBのデータを変更する。また、レスポンスをremixコンポーネントに渡す。 |
| 6 | ローダー | app/routes |
UIロジック、CSSスタイリングロジック、状態管理ロジック | ブラウザ操作に応じて、アクションからデータを取得する。 |
| 7 | remixコンポーネント | app/components |
UIロジック、CSSスタイリングロジック、状態管理ロジック | 2番に同じ |
▼ SSRの場合の詳細な流れ¶
以下は、SSR時にログイン後のトップページの表示に関わる処理である。
SSRのため、サーバーレンダリングとしている。
リクエスト受信:./app/entry.server.tsx
- クライアントがブラウザまたはBotかを判定
- UIレンダリングパターン選択
⬇︎
⬇︎
ミドルウェア処理:./app/middleware/tokenVerification.ts への切り分け
- アクセストークン署名検証
- 未認証時は認証を要求
⬇︎
⬇︎
データ処理:./app/routes + ./app/models
- ルーティング
- DB操作
⬇︎
⬇︎
ミドルウェア処理:./app/middleware/context.ts
- FlashMessage操作
- レスポンスの`Set-Cookie`ヘッダー操作
⬇︎
⬇︎
サーバーレンダリング:./app/routes + ./app/components
⬇︎
⬇︎
レスポンス返信:./app/entry.server.tsx
ローダー¶
▼ ローダーとは¶
ローダーは、loader 関数として定義できる。
レンダリング前にAPIからデータを取得し、またブラウザ操作に応じてアクションからデータを取得する。
各エンドポイントごとに定義できる。
DBにクエリを送信し、データを取得できる。
認証処理がある場合、ローダーの前に実行する必要がある。
*実装例*
import {json} from "@remix-run/node";
import {useLoaderData} from "@remix-run/react";
// ローダー
export const loader = async () => {
// バックエンドでデータを処理する
// ローダーの段階でデータを用意しておき、フロントエンドではデータ表示ロジックだけを実装すると、ブラウザの描画のパフォーマンスが上がる
const data = {
posts: [
{
slug: "my-first-post",
title: "My First Post",
},
{
slug: "90s-mixtape",
title: "A Mixtape I Made Just For You",
},
],
};
return json(data);
};
▼ ロギング¶
ローダー内で console.log 関数を実行すると、バックエンドの実行ログとして出力され、ブラウザのコンソールには出力されない。
▼ useLoaderData¶
ローダーで取得したデータを出力できる。
*実装例*
import {json} from "@remix-run/node";
import {useLoaderData} from "@remix-run/react";
// ローダーでレンダリング前にバックエンドでデータを処理する
export const loader = async () => {
// バックエンドでデータを処理する
// ローダーの段階でデータを用意しておき、フロントエンドではデータ表示ロジックだけを実装すると、ブラウザの描画のパフォーマンスが上がる
const data = {
posts: [
{
slug: "my-first-post",
title: "My First Post",
},
{
slug: "90s-mixtape",
title: "A Mixtape I Made Just For You",
},
],
}
return json(data);
};
// remixコンポーネントで、レンダリング処理を実行する
export default function Posts() {
const {posts} = useLoaderData<typeof loader>();
return (
<main>
<h1>Posts</h1>
</main>
);
}
remixコンポーネント¶
▼ remixコンポーネントとは¶
Reactを使用して実装されたRemix専用のビルトインコンポーネントである。
remixコンポーネントは、レンダリング処理を実行する。
import {json} from "@remix-run/node";
// 内部的にはreactコンポーネントである。
import {useLoaderData} from "@remix-run/react";
// ローダー
export const loader = async () => {
// バックエンドでデータを処理する
// ローダーの段階でデータを用意しておき、フロントエンドではデータ表示ロジックだけを実装すると、ブラウザの描画のパフォーマンスが上がる
const data = {
posts: [
{
slug: "my-first-post",
title: "My First Post",
},
{
slug: "90s-mixtape",
title: "A Mixtape I Made Just For You",
},
],
}
return json(data);
};
// remixコンポーネントで、レンダリング処理を実行する
export default function Posts() {
const {posts} = useLoaderData<typeof loader>();
return (
<main>
<h1>Posts</h1>
</main>
);
}
▼ ロギング¶
remixコンポーネント内で console.log 関数を実行すると、ブラウザのコンソールに出力され、バックエンドの実行ログには出力されない。
▼ アクションではなくremixコンポーネントに実装するべき処理¶
バックエンドのデータを変更する必要がないような外部API通信処理は、アクションではなくremixコンポーネントに実装するべきである。
アクション¶
▼ アクションとは¶
レンダリング後のブラウザ操作に応じて、デザインパターンのコントローラーのようにクエリストリングやリクエストのコンテキストを受信し、DBのデータを変更する。
また、レスポンスのデータをremixコンポーネントに渡す。
componentを同じファイルに実装する以外に、.server ディレクトリに切り分ける方法もある。
import type { ActionFunctionArgs } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import { Form } from "@remix-run/react";
import { TodoList } from "~/components/TodoList";
import { fakeCreateTodo, fakeGetTodos } from "~/utils/db";
// レンダリング後のブラウザ操作でactionが実行され、actionの結果を取得する
export async function loader() {
// ローダーでレンダリング前にバックエンドでデータを処理する
const data = await fakeGetTodos()
return json(data);
}
// remixコンポーネントで、レンダリング処理を実行する
export default function Todos() {
// useLoaderDataでloaderによる取得データを出力する
const data = useLoaderData<typeof loader>();
// Todoリストを出力する
return (
<div>
<TodoList todos={data} />
<Form method="post">
<input type="text" name="title" />
{/*
Create Todoボタンを設置する
同一ファイルのactionをコールする。
*/}
<button type="submit">Create Todo</button>
</Form>
</div>
);
}
// アクションで、受信したリクエストに応じたレスポンス処理を実行する
export async function action({request}: ActionFunctionArgs) {
const body = await request.formData();
try {
const todo = await fakeCreateTodo({
title: body.get("title"),
});
return redirect(`/todos/${todo.id}`);
} catch (error) {
// さまざまな型のerrorを処理できるように対処する
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(errorMessage, body.get("title"))
// error.statusが未定義の場合、一律で500ステータスとして扱う
return json({ error: errorMessage }, { status: error.status ?? 500});
}
}
▼ ロギング¶
アクション内で console.log 関数を実行すると、バックエンドの実行ログとして出力され、ブラウザのコンソールには出力されない。
03. アーキテクチャ¶
Remix独自のMVCアーキテクチャ¶
Remixは独自のMVCアーキテクチャを採用している。
各routeファイルが独立した小さなMVCになっている。
View
# routesのcomponentの処理などが相当
↓
↓
Controller
# routesのloaderやactionの処理が相当
↓
↓
Model
# modelsなどが相当
# ドメイン層のモデルとインフラストラクチャ層の永続化処理が結合している
レイヤードアーキテクチャ風¶
レイヤードアーキテクチャにしたらどうだろうか。
UserInterface層
# routesのcomponent、loader、actionの処理の中で、入力と出力の処理が相当
↓
↓
Application層
# routesのcomponent、loader、actionの処理の中で、modelsとの調整処理が相当
↓
↓
Domain層
# アプリケーション独自のモデルが相当
↓
↓
Infrastructure層
# データベース接続、ロギング、ファイルシステム操作、外部API通信などが相当
クリーンアーキテクチャ風¶
04. UIレンダリングパターン¶
Remixでは、SSRモード、CSRモード、SSGモードがある。
CSRモードとSSGモードは厳密ではなく、Remix独自の擬似的なモードである。
それぞれのモードで、entry.server.tsx ファイルと entry.client.tsx ファイルが関与する。
entry.server.tsx ファイル |
entry.client.tsx ファイル |
|
|---|---|---|
| SSR | サーバーレンダリング関連の処理 | ハイドレーション関連の処理 |
| 擬似的CSR | 最小限のサーバーレンダリング関連の処理 | クライアントレンダリング関連の処理 |
| 擬似的SSG | ビルド時にサーバーレンダリング関連の処理 | ハイドレーション関連の処理 |
05. ディレクトリ構成¶
構成¶
app ディレクトリ配下はユーザーで構成する必要があり、例えば以下のようにする。
.
├── app/
│ ├── components/ # フロントエンドで使用する汎用的な独自reactコンポーネント(remixコンポーネントではない)
│ │ ├── share/ # 各reactコンポーネントで使用するロジック
│ │ ├── decorator/ # 認証などの補助的なreactコンポーネント
│ │ ├── forms/ # 入力フォームreactコンポーネント
│ │ ├── layouts/ # 画面レイアウトreactコンポーネント
│ │ └── validators/ # 入力フォームの検証ロジック
│ │
│ ├── constants/ # フロントエンド/バックエンドで使用するグローバルな値
│ ├── entry.client.tsx
│ ├── entry.server.tsx
│ ├── hooks/ # フロントエンドの状態管理ロジックで使用する汎用的なsetState関数
│ ├── domain/ # 独自のドメインモデル (ドメインロジックのため、永続化処理以外には依存させない)
│ ├── repository/ # ORMモデルを使用した永続化処理
│ ├── openapi/ # OpenAPI仕様書の生成処理
│ ├── root.tsx
│ ├── routes/ # ページング処理とAPIルーティング処理の関数。関数はアクション、コンポーネント、ローダーに分類できる。
│ ├── usecases # routeディレクトリのapiルートまたはuiルートから呼び出すユースケース
│ ├── services/ # routesディレクトリで使用する『デザインパターン』『外部APIとの通信』『認証』『prisma.server.ts』など
│ ├── styles/ # CSS、Tailwind、など
│ └── utils/ # フロントエンド/バックエンドで使用する『薄い関数』『その他、汎用的な非機能ロジックの関数』など
│
├── prisma/ # モデルの定義
...
root.tsx¶
アプリケーションのルートである。
link タグ、meta タグ、script タグを定義する。
entry.client.tsx¶
全てのモードで使用する (CSRモードだけではない) 。
マークアップファイルのハイドレーション処理のエントリーポイントである。
entry.server.tsx¶
▼ entry.server.tsxとは¶
全てのモードで使用する (SSRモードやSSGモードだけではない) 。
レスポンス作成処理のエントリーポイントである。
RemixServer で設定を変更できる。
▼ default エクスポート¶
entry.server ファイルの default エクスポート関数は、RemixのSSRモードのエントリーポイントになる。
handleRequest という名前であることが多いが、どんな名前でもよい。
default エクスポート関数を複数定義することはできない。
▼ handleDataRequest¶
Remixの内部で実行され、JSONデータを作成し、Remixのフロントエンド処理に渡す。
useLoaderData 関数や useFetcher().data 関数で取得できる。
export function handleDataRequest(response: Response, {request, params, context}: LoaderFunctionArgs | ActionFunctionArgs
) {
response.headers.set("X-Custom-Header", "value");
return response;
}
レイヤードアーキテクチャにすると...¶
app
├── application
│ └── projects
│ ├── ports.ts
│ └── usecases.ts
│
├── domain
│ └── projects
│ ├── rules.ts
│ └── types.ts
│
├── infrastructure # modelsディレクトリのファイルをinfrastructureディレクトリに配置する
│ ├── prisma.server.ts
│ └── projects
│ ├── repo.server.ts # ドメインモデルをCRUDする
│ └── DTO.ts # ドメインモデルのデータをDTOに詰め替える
│
├── presentation # routesディレクトリのファイルをpresentationディレクトリに配置する
│ └── projects
│ ├── route.server.ts
│ ├── route.tsx
│ └── schema.ts
│
└── utils
06. セットアップ¶
React Router v6¶
Remix v2を @remix-run/node パッケージからインポートする。
import {redirect} from "@remix-run/node";
React Router v7以降¶
Remix自体がReact Routerに統合されたため、react-router パッケージをインポートする。
import {redirect} from "react-router";
07. ルーティング¶
uiルートとapiルート¶
Remixでは、ブラウザルーティングとAPIエンドポイントを区別せず、両方を兼ねている。
ただし、ファイル名によって区別できる。
app/routes/api.<任意のパス> ファイルまたは app/routes/api/<任意のパス> ファイルを作成する。
このファイルの処理は、APIとして処理される。
ドッド分割¶
▼ _index.tsx¶
ルートパスになる。
app/ # URLパス
├── routes/
│ ├── _index.tsx # /
│ ...
│
└── root.tsx
▼ <ルート以降のパス>.tsx¶
ルート以降のパスを設定する。
app/ # URLパス
├── routes/
│ ├── _index.tsx # /
│ ├── home.tsx # /home
│ ├── home.contents.tsx # /home/contents
│ ...
│
└── root.tsx
*実装例*
// <ルート以降のパス>._index.tsx
export default function Foo() {
// 返却するHTML要素
return (
<main>
<h1>Foo</h1>
</main>
);
}
▼ <ルート以降のパス>.<変数>.tsx (動的セグメント)¶
動的にURLを決定する。
URLに規則性があるようなページに適する。
app/ # URLパス
├── routes/
│ ├── _index.tsx # /
│ ├── home.tsx # /home
│ ├── home.contents.tsx # /home/contents
│ ├── user.$id.tsx # /user/{任意の値}
└── root.tsx
*実装例*
// posts.$postId.tsxファイル
export default function Post() {
return (
<div>
<h1 className="font-bold text-3xl">投稿詳細</h1>
</div>
);
}
以下のURLでページをレンダリングできる。
- /posts/1
- /posts/2
- /posts/3
▼ 子の _<ルート以降のパス>.tsx¶
子のファイル名にプレフィクスとして _ (パスレスルート) をつける。
これにより、親からレイアウトを引き継ぎつつ、パスは引き継がない。
*実装例*
_home.auth.tsx ファイルは、親の home.tsx ファイルのレイアウトを引き継いでいる。
しかし、/home/auth パスではなく、/auth パスになる。
app/ # URLパス 引き継ぐレイアウト
├── routes/
│ ├── _index.tsx # / root.tsx
│ ├── home.tsx # root.tsx
│ ├── home._index.tsx # /home home.tsx
│ ├── home.contents.tsx # /home/contents home.tsx
│ ├── home_.mine.tsx # /home/mine root.tsx
│ ├── _home.auth.tsx # /auth home.tsx # 親からパスを引き継がない
│ ├── user.$id.tsx # /user/{任意の値} root.tsx
│ ...
│
└── root.tsx
▼ 親の _<ルート以降のパス>.tsx (親がパスレスルート)¶
親のファイル名にプレフィクスとして _ (パスレスルート) をつける。
*実装例*
_auth.<任意の名前>.tsx ファイルは、親の _auth.tsx ファイルのレイアウトを引き継いでいる。
しかし、全てのファイルのURLに auth が含まれない。
app/ # URLパス
├── routes #
│ ├── _auth.tsx #
│ ├── _auth.login.tsx # /login
│ ├── _auth.password.reset.tsx # /password/reset
│ ├── _auth.register.tsx # /register
...
// _auth.tsxファイル
import {Outlet} from "@remix-run/react";
import {SiteFooter, SiteHeader} from "~/components";
export default function AuthCommon() {
return (
<div className="grid grid-rows-[auto_1fr_auto] h-dvh">
<SiteHeader />
{/* Outletに子 (login、password.reset、register) を出力する */}
<Outlet />
<SiteFooter />
</div>
);
}
ディレクトリ分割¶
▼ 基本ルール¶
ディレクトリ名がパスとして認識される。
▼ サブディレクトリ¶
以下によると、Remix v2 はデフォルトでは routes にサブディレクトリを作れない。
- https://github.com/remix-run/remix/discussions/8473#discussioncomment-8084973
- https://v2.remix.run/docs/file-conventions/routes#folders-for-organization
ただ、https://github.com/kiliman/remix-flat-routes を使うと、サブディレクトリを作ることはできる。
app/routes-hybrid-files/
├── _auth+
│ ├── forgot-password.tsx
│ └── login.tsx
├── _public+
│ ├── _layout.tsx
│ ├── about.tsx
│ ├── contact[.jpg].tsx
│ └── index.tsx
├── project+
│ ├── _layout.tsx
│ ├── parent.child
│ │ └── index.tsx
│ └── parent.child.grandchild
│ ├── index.tsx
│ └── styles.css
└── users+
├── $userId.tsx
├── $userId_.edit.tsx
├── _layout.tsx
└── index.tsx
Remixの仕様ではディレクトリ構造やファイル名がエンドポイントに影響してしまう。
エンドポイントを崩さずに routes にサブディレクトリを作る場合、ファイル名を部分的に変えないといけず、エンドポイントが一目ではわかりにくくなる。
現在の状態であれば、ファイルは多くて辛いが、エンドポイントがファイル名から一目でわかる。
よって、ひとまず現在のままとする。
(Remixは、規模が大きくなると routes がしんどくなる問題がありそう...)
Remixはとても小さなサイトや個人のブログだけを想定しているのですか?Remixは全般的に好きだけど、フラットルートを使うようになったことで、正直Remixが使いづらくなった。
https://github.com/remix-run/remix/discussions/8473#discussioncomment-9174522
08. remixコンポーネントの種類¶
ユーザー定義¶
Remixがコンポーネントであることを認識するために、名前の先頭を大文字する。
Remix Formコンポーネント¶
form タグをレンダリングする。
action 値を省略した場合、フォームの入力データは他に送信されず、そのremixコンポーネント内のみで処理される。
action 値を /foos?index パスとした場合、routes/foos/index.jsx ファイルにデータを送信する。
一方で、action 値を /foos パスとした場合、routes/foos.jsx ファイルにデータを送信する。
*実装例*
ここでは action 値を省略している。
import {Form} from "@remix-run/react";
function NewEvent() {
return (
<Form action="/events" method="post">
<input name="title" type="text" />
<input name="description" type="text" />
</Form>
);
}
Remix Linkコンポーネント¶
URLを書き換える。
ページネーション処理に使える。
LinkコンポーネントがURLクエリストリングのページ値を書き換えると、Remixのローダーがそれを検知し、新しいURLクエリストリングで再リクエストを送信することで別のページが表示される。
*実装例*
import {Link, useSearchParams} from "@remix-run/react";
type PageLinkProps = {
page: number;
children: React.ReactNode;
resetKeys?: string[];
};
export function PageLink({page, children, resetKeys = []}: PageLinkProps) {
const [searchParams] = useSearchParams();
const next = new URLSearchParams(searchParams);
for (const key of resetKeys) {
next.delete(key);
}
next.set("page", String(page));
// LinkコンポーネントがURLクエリストリングのページ値を書き換える
return (
<Link
to={{search: `?${next.toString()}`}}
preventScrollReset
prefetch="intent"
>
{children}
</Link>
);
}
// RemixのローダーがURLの変更を検知する
export async function loader({request}: LoaderFunctionArgs) {
// 新しいクエリストリングをURLから取得する
const url = new URL(request.url);
const page = Number(url.searchParams.get("page") ?? "1");
return typedjson({
// 新しいクエリストリングで再リクエストを送信する
}
}
Remix Metaコンポーネント¶
Webページの meta タグ (Webサイト名、説明など) をレンダリングする。
import {Meta} from "@remix-run/react";
export default function Root() {
return (
<html>
<head>
<Meta />
</head>
<body></body>
</html>
);
}
Remix Outletコンポーネント¶
親ページ内に子ページをレンダリングする。
import {Outlet} from "@remix-run/react";
export default function SomeParent() {
return (
<div>
<h1>Parent Content</h1>
<Outlet />
</div>
);
}
09. Cookieを使用した認証¶
LocalStorageやSessionStorageではなくCookie¶
RemixはSSRアプリケーションを作成する。
SSRでは、Web Storage APIと通信できず、ブラウザのLocalStorageやSessionStorageを操作できない。
代わりに、ブラウザのCookie、サーバーのメモリ、サーバー上のファイルなどに資格情報を保存することになる。
Cookieの作成と保存¶
▼ ブラウザのCookieに保存する場合¶
ブラウザのCookieに資格情報を保存する。
export const cookieSessionStorage = createCookieSessionStorage({
cookie: {
name: "__session",
httpOnly: true,
path: "/",
sameSite: "lax",
secrets: [process.env.SESSION_SECRET],
},
});
▼ サーバーのメモリに保存する場合¶
サーバーのメモリに資格情報を保存する。
export const memorySessionStorage = createMemorySessionStorage({
cookie: sessionCookie,
});
▼ サーバー上のファイルに保存する場合¶
サーバー上のファイルに資格情報を保存する。
export const memorySessionStorage = createFileSessionStorage({
dir: "/app/sessions",
cookie: sessionCookie,
});
10. モデル¶
prismaによるスキーマ¶
Prisma ORMを使用して、データベースのスキーマを定義する。
model User {
id String @id @default(cuid())
name String
email String @unique
role String // "ADMIN" | "MEMBER"
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
CRUD処理¶
▼ CREATE¶
ドメインモデルを作成する。
import type {User} from "@prisma/client";
import {prisma} from "~/services/prisma.server";
// CREATE
export async function createUser({
name,
email,
role,
}: {
// prismaのスキーマを使用して、引数の型を指定する
name: User["name"];
email: User["email"];
role: User["role"];
// prismaのスキーマを使用して、返却値の型を指定する
}): Promise<User> {
return prisma.user.create({
data: {name, email, role},
});
}
▼ READ¶
DBレコードからドメインモデルを取得する。
import type {User} from "@prisma/client";
import {prisma} from "~/services/prisma.server";
// READ One
export async function getUserById({
id,
}: {
// prismaのスキーマを使用して、引数の型を指定する
id: User["id"];
// prismaのスキーマを使用して、返却値の型を指定する
}): Promise<User | null> {
return prisma.user.findUnique({
where: {id},
});
}
// READ Many
export async function listUsers(): Promise<User[]> {
return prisma.user.findMany({
orderBy: {createdAt: "desc"},
});
}
▼ UPDATE¶
ドメインモデルを変更する。
import type {User} from "@prisma/client";
import {prisma} from "~/services/prisma.server";
// UPDATE
export async function updateUser({
id,
name,
email,
role,
}: {
// prismaのスキーマを使用して、引数の型を指定する
id: User["id"];
name?: User["name"];
email?: User["email"];
role?: User["role"];
// prismaのスキーマを使用して、返却値の型を指定する
}): Promise<User> {
return prisma.user.update({
where: {id},
data: {
...(name !== undefined ? {name} : {}),
...(email !== undefined ? {email} : {}),
...(role !== undefined ? {role} : {}),
},
});
}
▼ DELETE¶
ドメインモデルを削除する。
import type {User} from "@prisma/client";
import {prisma} from "~/services/prisma.server";
// DELETE
export async function deleteUser({
id,
}: {
// prismaのスキーマを使用して、引数の型を指定する
id: User["id"];
// prismaのスキーマを使用して、返却値の型を指定する
}): Promise<User> {
return prisma.user.delete({
where: {id},
});
}
11. エラー¶
バックエンド¶
▼ エラーハンドリング¶
| データ名 | 説明 | 例 |
|---|---|---|
| state | ステータスコード | 405 |
| statusText | ステータスコードのエラーメッセージ | Method Not Allowed |
| data | 詳細なエラー | Error: ***** |
フロントエンド¶
▼ ユーザー向けのメッセージ¶
import {
ExclamationTriangleIcon,
LockClosedIcon,
MagnifyingGlassIcon
} from "@heroicons/react/20/solid";
import { isRouteErrorResponse, useRouteError } from "@remix-run/react";
import React from "react";
export function ErrorBoundary() {
// ローダーやアクションのエラーステータスを取得する
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
<div>
<div
className="flex h-[80%] w-full items-center justify-center p-5"
data-cy="errorBoundary"
>
<div className="text-center">
<div className="inline-flex justify-center p-3">
{/* 403ステータスの場合に使用するアイコン */}
{error.status === 403 && (
{/* 南京錠アイコン */}
<LockClosedIcon className="h-[20%] w-[20%] text-yellow-500" />
)}
{/* 404ステータスの場合に使用するアイコン */}
{error.status === 404 && (
{/* 虫眼鏡アイコン */}
<MagnifyingGlassIcon
className="h-[20%] w-[20%] text-sky-400"
data-cy="notFoundError"
/>
)}
</div>
<p className="mt-2 text-[24px] font-bold text-slate-800 lg:text-[38px]">
{/* ローダーやアクションのエラーステータスのタイトルを出力する */}
{error.data.title}
</p>
<p className="mt-5 text-slate-600 lg:text-lg">
{/* ローダーやアクションのエラーステータスのメッセージを出力する */}
{error.data.message}
</p>
</div>
</div>
</div>
);
}
return (
<div>
<div className="flex h-[80%] w-full items-center justify-center p-5">
<div className="text-center">
<div className="inline-flex justify-center p-3">
{/* 異常アイコン */}
<ExclamationTriangleIcon className="h-[20%] w-[20%] text-red-600" />
</div>
<p className="mt-2 text-[24px] font-bold text-slate-800 lg:text-[38px]">
500 Internal Server Error
</p>
<p className="mt-5 text-slate-600 lg:text-lg">
予期せぬエラーが発生しました
</p>
</div>
</div>
</div>
);
}
// Remixでは、ErrorBoundaryという名前でコールする必要がある
export {ErrorBoundary} from "~/components/ErrorBoundary";