kanachi-blog

notionでの公開記事をastro-notion-blogを使って公開するよ

remix 基礎 備忘録

Remixとは

Remixは、Reactをベースとしたフルスタックフレームワークです。

フルスタックフレームワークを使っているというよりは、Reactで開発しながら、サーバーサイドの処理も同時に書けるのがRemixです。

Remix はこれらすべてのコンポーネントを事前にレンダリング(SSR)するため、サーバー上で完成したページをエンド ユーザーに提供します。

Remix vs NextJS
Remix NextJS
レンダリング 常にSSR SSRはオプション
サイト生成 静的サイト生成なし(ビルド時のプリレンダリングなし) 静的サイト生成(ビルド時)がサポートされている
ホスト要件 常にサーバーサイドコード実行をサポートするホストが必要 静的ホスティングとサーバーサイドコード実行のデプロイオプションがある

Remixのコアコンセプト

remixでのルーティング

app配下のデフォルトの構造

app
 ┣ routes
 ┃ ┣ _index.tsx
 ┃ ┗ hoge.tsx
 ┣ entry.client.tsx
 ┣ entry.server.tsx
 ┗ root.tsx
  • routes 配下に作成したファイルはそのままURL にマッピングされる。
    URL Matched Routes
    / app/routes/_index.tsx
    /hoge app/routes/hoge.tsx
  • ページ内の遷移にはLinkを用いる、Linkはclient side routingを採用しているのでページ全体のリクエストは行われない。
    import { Link } from "@remix-run/react";
    
    export default function Sample() {
      return (
        <>
          <h1>Sample Page</h1>
          <Link to="/hoge">Go to hoge Page</Link>
        </>
      );
    }
    📖
    client side routingとは
    ページ間のページ遷移がブラウザ上(クライアントサイド)で行われることを指す。
    この方法では、新しいページへのリンクがクリックされると、ブラウザは新たにページ全体をサーバから取得するのではなく、必要なデータだけを取得(通常はAPI経由)し、それを用いて新しいページをレンダリングする。

    参考
Styleの設定
  1. Remix でPlaneなCSSを使用する場合
    • Styleの設定にはlinksを利用します。詳細は下記を参照。
      📖
      Remixにおけるlinks機能の使用について
      1. root.tsxでのLinksの使用:
        root.tsxはアプリケーション全体の基盤となるコンポーネントです。ここで定義される<Links />は、全てのページで共通して適用されるCSSなどのリソースを含みます。これにより、アプリケーション全体にわたる共通のスタイルや設定を行うことができます。
      2. routesディレクトリでのlinksの使用:
        routesディレクトリ内の各ファイルでは、その特定のルートに関連するリソースを定義することができます。ここでLinksFunctionを使ってリンクを定義すると、Remixはそのルートがアクティブになったときに自動的にそれらのリンクを<head>追加します。つまり、各ルートで定義されたスタイルやリソースは、そのルートがロードされるときにのみ適用され、ルートごとに異なるスタイリングやリソースを持たせることができます。
    • ComponentへのStyleの適用に関しては下記参照

      Componentで設定したstyleは結局はページで全て適用されるので、スコープはComponentに閉じないので、classNameの重複に注意が必要です。

  2. Remix でCSS moduleを利用する場合
データの送受信
  • 送信

    データの送信にはactionを使います。

    データの変更やその他のアクションを処理するサーバー専用の機能です。GETルート以外のmethod( DELETEPATCHPOST)でリクエストが行われた場合に、actionloader の前に呼び出されます。

    フォーム要素もremix独自のFormを用いることで、client side routingを採用しているのでページ全体のリクエストは行われない。

  • 受信

    データの受信にはloaderを使います。

    データの受信を行うサーバー専用の機能です。GETでリクエストが呼び出された時に実行されます。コンポーネント内で受信したデータを利用する場合には、useLoaderData()を利用してデータを受け取ります。

    export default function NotesPage() {
      const notes: Note[] = useLoaderData();
      return (
        <main>
          <NewNote />
          <NoteList notes={notes} />
        </main>
      );
    }
    
    export async function loader() {
      const notes = await getStoredNotes();
      return json(notes);
    }
バリデーション

Validationを行う先には、Action内でデータの検証を行います。

データにエラーがあり、独自のエラーメッセージを返してブラウザで表示させたい場合には、Actionはサーバーサイドで実行されるため、Windowメソッドなどを呼び出すことはできません。

エラーが発生した場合にはAction内でObjectをリターンします。

useActionDataを使いブラウザにメッセージ出力用のデータなどを取得します。

連打処理を行う際には、useNavigationを使用します。

useNavigationでは、保留中のページ ナビゲーションに関する情報を提供します。

isSubmittingなどの送信中の状態も確認することができ、送信中の間にはbutton を無効にするなどして連打処理を実現できます。

具体例

export async function action({ request }: ActionFunctionArgs) {
  const formData = await request.formData();
  const noteData: Note = {
    id: new Date().toISOString(),
    title: formData.get("title") as string, 
    content: formData.get("content") as string, 
  };

  if (noteData.title.trim().length < 5) {
    return { message: "タイトルは5文字以上で入力してください" };
  }

  const existingNotes = await getStoredNotes();
  const updatedNotes = existingNotes.concat(noteData);
  await storeNotes(updatedNotes);
  return redirect("/notes");
}
import type { action } from "~/routes/notes";

export function NewNote() {
  const data = useActionData<typeof action>();
  const navigation = useNavigation();
  const isSubmitting = navigation.state === "submitting";

  return (
    <Form method="post" id="note-form">
      {data?.message && <p>{data.message}</p>}
      <p>
        <label htmlFor="title">Title</label>
        <input type="text" id="title" name="title" required />
      </p>
      <p>
        <label htmlFor="content">content</label>
        <textarea id="content" name="content" rows={5} required />
      </p>
      <div className="form-actions">
        <button disabled={isSubmitting}>
          {isSubmitting ? "Adding..." : "Add Note"}
        </button>
      </div>
    </Form>
  );
}
エラーハンドリング

共通的なエラーの処理を行う場合は、ErrorBoundaryを利用します。

  • Remix は、コード内のほとんどのエラーをサーバー上またはブラウザ上で自動的に検出し、エラーが発生した場所に最も近いErrorBoundaryをレンダリングします。
  • useRouteErrorからはactionloader、またはレンダリング中にスローされたエラーにアクセスすることができます。

    具体例

    • rendering in the browser
    • rendering on the server
    • in a loader during the initial server-rendered document request
    • in an action during the initial server-rendered document request
    • in a loader during a client-side transition in the browser (Remix serializes the error and sends it over the network to the browser)
    • in an action during a client-side transition in the browser
  • カスタムエラーを表示させる場合には、エラー発生時にthrowをして特定のエラーを返します。そしてErrorBoundary内でisRouteErrorResponseを使い、そのエラーを表示させます。
    //エラーをthrowする
    export async function loader() {
      const notes = await getStoredNotes();
      if (!notes || notes.length === 0) {
        throw json(
          { message: "ノートが見つかりませんでした。" },
          { status: 404, statusText: "Not Found" }
        );
      }
      return notes;
    }
    import {
      useRouteError,
      isRouteErrorResponse,
    } from "@remix-run/react";
    
    export function ErrorBoundary() {
      const error = useRouteError();
    
      // when true, this is what used to go to `CatchBoundary`
      if (isRouteErrorResponse(error)) {
        return (
          <div>
            <h1>Oops</h1>
            <p>Status: {error.status}</p>
            <p>{error.data.message}</p>
          </div>
        );
      }
    
      // Don't forget to typecheck with your own logic.
      // Any value can be thrown, not just errors!
      let errorMessage = "Unknown error";
      if (isDefinitelyAnError(error)) {
        errorMessage = error.message;
      }
    
      return (
        <div>
          <h1>Uh oh ...</h1>
          <p>Something went wrong.</p>
          <pre>{errorMessage}</pre>
        </div>
      );
    }
Routes File Naming

Routes配下のファイルの命名規則についてはこちらを参照

  • URL内の/はroutes配下では. で表現します。
  • 動的なURLは$ で表現します。
  • .区切りでネストしたルートを作成します。.の前のファイル名が他のルートのファイル名にマッチする場合、そのルートは自動的にマッチする親の子ルートになります。
  • URL をネストしたいが、自動レイアウトのネストは望まない場合があります。親セグメントの末尾に_を付けてネストをオプトアウトできます。
  • セグメントの末尾に_を付けると、ファイル名を覆い、URL からファイル名を隠すブランケットとなります。
  • ルートセグメントを()で囲むと、セグメントがオプションになります。e.g. ($lang)
  • $は単一のパス セグメント (URL 内の 2 つのセグメントの間にあるもの) と一致しますが、Splat route(e.g. files.$.tsx)はスラッシュを含む URL の残りの部分と一致します。
  • Remix がこれらのルート規則に使用する特殊文字の 1 つを実際に URL の一部にしたい場合は、[]文字を使用して規則をエスケープできます。
Metaデータの設定

特定のルートでmeta関数を設定することで、サイトのmeta情報を設定できます。

親ルートと小ルートが競合する場合は、より下位の小ルートの設定が優先されます。

export const meta: MetaFunction<typeof loader> = ({ data }) => {
  return [
    {
      title: data ? data.title : "Loading...",
      description: "Manage your notes with ease.",
    },
  ];
};