コンポーネントに関する設定
まずは、ルートモジュールに設定できるReactコンポーネントについて解説します。この区分に分類できるのは、表1の3つです。
| 名称 | 概要 |
|---|---|
| デフォルト | このルートモジュールが通常の方法で表示するコンポーネント |
| ErrorBoundary | ルートモジュール内で起きたエラーをハンドリングしてエラー表示を出すためのコンポーネント |
| HydrateFallback | 初期表示時のclientLoader(※)が完了するまでの間に表示するコンポーネント |
Reactコンポーネントなので、設定しておくと、フレームワーク側から何らかのタイミングで呼び出され、UIとして表示されることになります。
(※)ブラウザ側でデータフェッチやWeb APIの実行を行い、コンポーネントにデータを渡すための関数
デフォルト
デフォルトのエクスポート、つまり export default キーワードによって外部へ公開されるコンポーネントは、ブラウザで表示するURLがそのルートモジュールのファイルパスにマッチしたときに表示されるUIを表します。もう本連載の中では見慣れたものですね(リスト1)。
export default function DefaultExportedComponent() {
return (
<main>
<h1>Default Exported Component</h1>
<span>このコンポーネントは<code>/default</code>のパスで表示されます</span>
</main>
);
}
サンプルコードを手元で実行して、リスト1に該当する http://localhost:3000/default にアクセスすると、図1の画面が表示されます。
注意すべき点として、このコンポーネントを表す関数は、サーバーで実行する(サーバーサイドレンダリング)のかブラウザで実行されるのかが、表示時の文脈による点があります。ブラウザのアドレスバーに直接URLを入力して表示する場合はサーバーで実行されますし、一度表示された後でサイト内を画面遷移するだけの場合はブラウザで実行される可能性が高いです。サーバーかブラウザのどちらかでしか実行できないAPIを扱ってエラーになるのは初学者の頻出ミスですので、注意してください。具体的には、ブラウザでしか実行できない window.navigator や、Node.jsサーバーでしか実行できない fs などをコンポーネントの中で使用すると、エラーが起こりやすくなります。
なお、この問題は、loader と clientLoader を活用することで概ね解決できます。APIの詳細は次回で解説しますが、リスト2のように活用するのがオススメです。
import { useLoaderData } from '@remix-run/react';
import { json } from '@remix-run/node';
// サーバー側で実行されるデータ取得処理
export async function loader({ params }) {
// URLのパスからIDを取得
const { id } = params;
// (1) サーバーのファイルシステムからデータを取得
const items = await loadFile(id);
// ブラウザへデータを返す
return json({
items,
});
}
// クライアント側で実行されるデータ取得処理
export async function clientLoader({ params, serverLoader }) {
// loaderの実行結果を取得
const serverData = await serverLoader();
// URLのパスからIDを取得
const { id } = params;
// (2) ブラウザのローカルストレージに保存していた設定値を取得
const localSettings = window.localStorage.getItem(`Settings:${id}`);
// コンポーネントへデータを渡す
return {
localSettings,
...serverData,
};
}
export default function Component() {
// (3) clientLoaderが返したデータを取得
const { items, localSettings } = useLoaderData();
return (/* 省略 */);
}
(1)の loadFile() 関数は、サーバーのファイルシステムからデータを取得する処理を表しています。このようなサーバー側でしか実行できない処理は、loader で実行するようにします。一方、(2)はブラウザのローカルストレージからデータを取得する処理を表しています。このようなブラウザ側でしか実行できない処理は、clientLoader で実行するようにします。そして、(3)の useLoaderData() フックを使って、loader と clientLoader が返したデータを取得しています。このように、loader と clientLoader を使い分けることで、ブラウザやサーバーに固有のAPIを扱いつつ、コンポーネントからは同じようにデータを取得できるようになります。
ErrorBoundary
次は ErrorBoundary です。Reactの公式ドキュメントでも、同名のデザインパターンが紹介されているので、名前を見たことがある方もいるかもしれません。
Reactツリーのレンダリング中、つまりコンポーネントの関数やクラスを評価・実行している間に発生したエラーをキャッチして、ログ基盤にクラッシュレポートを送ったり、ユーザーにエラー画面を表示するためのデザインパターンが ErrorBoundary ですが、Remixでも同様に、ルートモジュール内で起きたエラーを上手くハンドリングするための機構として、同名のコンポーネントを定義することができます。Remixではリスト3のように定義します。
import {
isRouteErrorResponse,
useRouteError,
} from "@remix-run/react";
// ErrorBoundaryコンポーネントを定義する
export function ErrorBoundary() {
// (1) ルートモジュール内で発生したエラーを取得する
const error = useRouteError();
// (2) 通信エラーかどうかを判定する
if (isRouteErrorResponse(error)) {
// (3) 通信エラーに応じたUIを表示する
return (
<div>
<h1>
{error.status} {error.statusText}
</h1>
<p>{error.data}</p>
</div>
);
} else if (error instanceof Error) {
// (4) 通信エラーではないがエラーオブジェクトではある場合、エラーの内容を表示する
return (
<div>
<h1>エラーが発生しました</h1>
<p>{error.message}</p>
<p>スタックトレース:</p>
<pre>{error.stack}</pre>
</div>
);
} else {
// (5) 通信エラーでもエラーオブジェクトでもない場合、不明なエラーとして扱う
return <h1>Unknown Error</h1>;
}
}
Remixの ErrorBoundary は、ルートモジュール内で起きたエラー全般に関心を持ちます。そのため、Reactコンポーネントのレンダリングだけではなく、loader や action といった通信処理の実行中に発生したエラーもキャッチします。リスト3では、(1)の useRouteError フックを使って、ルートモジュール内で発生したエラーを取得しています。(2)では、取得したエラーが通信エラーかどうかを判定して、通信エラーであれば、(3)のようにエラーの内容を表示するUIを返します。通信エラーでない場合は、(4)のようにエラーの内容を表示するUIを返します。通信エラーでもエラーオブジェクトでもない場合は、(5)のように不明なエラーとして扱います。
例えば、デフォルトのコンポーネント内に throw new Error("I am error") というコードを埋め込んで、エラーオブジェクトによるエラーを発生させると、図2のようにエラー画面が表示されます。
この ErrorBoundary は、通常のUI(=デフォルトのコンポーネント)の表示がエラーによって不可能になった場合に、代わりに表示するコンポーネントとして機能します。Nested Routesを組んでいる場合には、エラーになったルートモジュールの ErrorBoundary だけが表示され、それより外側のレイアウト(多くの場合はメニューなどのUI)は引き続き使用することができるメリットがあるため、Nested Routesを組む場合には細かく設定しておくとよいでしょう。
HydrateFallback
コンポーネント区分の最後は、HydrateFallback です。これは、初期表示時にブラウザ側で実行されるloaderである clientLoader が完了するまでの間に、デフォルトコンポーネントの代わりに表示するコンポーネントを定義するためのものです。リスト4のように定義します。
import { useLoaderData } from "@remix-run/react";
// (1)
export async function clientLoader() {
const url = import.meta.url;
// Hydrate Fallbackを確認するために10秒待つ
await new Promise((resolve) => setTimeout(resolve, 10000));
return {
url,
}
}
// (2)
export function HydrateFallback() {
return <p>Loading Browser Settings...</p>;
}
export default function Component() {
const { url } = useLoaderData();
return (
<main>
<h1>Hydrate Fallback</h1>
<span>現在のURL: {url}</span>
</main>
);
}
HydrateFallback が表示されたときに確認しやすいよう、(1)の clientLoader では、10秒間待機するようにしています。このコンポーネントを表す関数は、(2)の HydrateFallback として定義します。このコンポーネントは、初期表示時に clientLoader が完了するまでの間に表示されます。リスト4のように定義しておくと、初期表示時に図3の画面が表示されます。
このまま10秒待つと、次は図4のように、HydrateFallback ではなく、デフォルトのコンポーネントが表示されます。
実際には10秒も待つことはないと思いますが、もし clientLoader の待ち時間が長くなりそうな場合は、HydrateFallback を定義しておくと、ユーザーに「フリーズしているのではなく、必要な処理を行っているので待っていてほしい」ことを伝えることができます。通説ですが、200ミリ秒を超えるとユーザーが「待っている」と感じ始めるので、ひとつの目安にしてみてください。
注意点として、loader と clientLoader を併用している場合は、clientLoader 関数に特殊なプロパティを定義しないと、HydrateFallback が表示されません。HydrateFallback を使いたい場合は、リスト5のように定義します。
export async function loader() {
// (省略)
}
export async function clientLoader({ serverLoader }) {
// (省略)
}
clientLoader.hydrate = true; // (1)
export function HydrateFallback() {
// (省略)
}
(1)のように、clientLoader 関数に hydrate = true というプロパティを定義しておくと、HydrateFallback が表示されるようになります。詳細は、公式ドキュメントを参照してください。
まとめ
今回は、ルートモジュールに設定できる各種の設定のうち、コンポーネントやデータ授受に関するものを解説しました。アプリケーションの機能に関わるものが中心でしたね。
次回はデータ授受に関する関数や、HTTPヘッダーの設定やHTMLの <head> 要素に関する設定などを中心に解説します。お楽しみに。
