Open24

remix触ってみる

uretanouretano
uretanouretano

Web標準に準拠することを重視した設計されている。

  • データを取得するときはFetch API
  • データミューテーションを行うときはFormタグ
  • サーバーにデータを持たせるときはクッキーとセッション
uretanouretano
uretanouretano

setup

$ npx create-remix@latest --template remix-run/remix/templates/remix-tutorial
$ cd my-remix-app
$ yarn install
uretanouretano

テンプレートで生成されたファイルを見てみる

app/root.tsx
import {
  Form,
  Links,
  Meta,
  Scripts,
  ScrollRestoration,
} from "@remix-run/react";

export default function App() {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        <div id="sidebar">
          <h1>Remix Contacts</h1>
          <div>
                // 省略
          </div>
        </div>

        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

最初にレンダリングされる UI 。ページのグローバルレイアウトが含まれている。

htmlタグ の中に headタグ > meta 情報, bodyタグ > コンポーネントと<ScrollRestoration />, <Scripts /> という remix から提供されている JSX タグ が含まれている

uretanouretano
package.json
//
"scripts": {
    "build": "remix vite:build",
    "dev": "remix vite:dev",
    "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
    "start": "remix-serve build/server/index.js",
    "typecheck": "tsc"
  },
//

build: コンパイルして build ディレクトリにファイルを出力する。
dev: localhost:5173 に開発サーバーをたてる。ホットリロードが効いている。
start: build 済みのファイルを参照して開発サーバーをたてる。ホットリロードが効かない。

package.json の scripts をみると、build, dev コマンドで remix vite:build/dev と書いてあり、内部的に vite が使われている。

uretanouretano

スタイルを設定する

プレーンな CSS ファイルを JavaScript モジュールに直接 import 可能。
以下を import し、追記すると CSS を読みこみスタイルが当てられる。

app/root.tsx
+ import type { LinksFunction } from "@remix-run/node";
+ import appStylesHref from "./app.css?url";

+ export const links: LinksFunction = () => [
+   { rel: "stylesheet", href: appStylesHref },
+ ];
uretanouretano

ルーティング

ファイルベースルーティングが可能なので、app/routes 配下にファイルを作成する。

$ mkdir app/routes
$ touch app/routes/contacts.\$contactId.tsx

https://remix.run/docs/en/main/file-conventions/routes

remix ではファイル名の.でURLを分割する。
app/routes/contacts.$contactId.tsx の場合 URL は contacts/123, contacts/abc といった動的パラメータに対応したページへアクセス可能となる。

uretanouretano

子ルートを親レイアウト内でレンダリングするには、親内で をレンダリングする必要があるので、root.tsx<Outlet /> を追加する。

root.tsx
+ import { Outlet } from "@remix-run/react";
export default function App() {
  return (
    <html lang="en">
      <head>・・・</head>
      <body>
        <div id="sidebar">
         ・・・
        </div>
+        <div id="detail">
+          <Outlet />
+        </div>
        ・・・
    </html>
  );
}
uretanouretano

データフェッチング

loderという非同期関数を作成し、コンポーネント内でuseLoaderDataを使って呼び出します。

root.tsx
・・・
export const loader = async () => {
  const contacts = await getContacts();
  return json({ contacts });
};

export default function App() {
  const { contacts } = useLoaderData<typeof loader>();

  return ( ・・・ )
}  

getContactsから取得した contacts を useLoader 経由でコンポーネント内で使えるようになります。

uretanouretano

ローダーでのURL params

contacts/contactId で URL パラメータから params を取得して、 loader からデータを取得するには、

contacts.$contactId.tsx
export const loader = async ({ params }: LoaderFunctionArgs) => {
  invariant(params.contactId, "Missing contactId param"); 
  const contact = await getContact(params.contactId);
  //contact が取得できなかった場合エラーを throw する
  if (!contact) {
    throw new Response("Not Found", { status: 404 });
  }
  return json({ contact });
};

export default function Contact() {
  const { contact } = useLoaderData<typeof loader>();

  return ( ・・・ )
}
uretanouretano

データの作成

action を作成する。

root.tsx
+ export const action = async () => {
+   const contact = await createEmptyContact();
+   return json({ contact });
+ };

export default function App() {
    return ( ・・・ )
}
<Form method="post">
    <button type="submit">New</button>
</Form>

<Form method="post"> で囲われた button タグをクリックすることでactionが発火する。

uretanouretano

データの更新

編集ページを作成する

$ touch app/routes/contacts.\$contactId_.edit.tsx

URL をネストしたいが、自動レイアウト ネストは不要という場合、親セグメントの末尾にアンダースコアを付けるとネストをオプトアウトできる。
/contacts/${contactId}/edit で対象のユーザのデータを更新するページへアクセスできる。

uretanouretano

action 関数を作成する。

contacts.contactId_.edit.tsx
export const action = async ({ params, request }: ActionFunctionArgs) => {
  invariant(params.contactId, "Missing contactId param");
  const formData = await request.formData();
  const updates = Object.fromEntries(formData);
  await updateContact(params.contactId, updates);
  return redirect(`/contacts/${params.contactId}`);
};

remix が提供する <Form> タグの子要素の input タグから
Formタグは <Form method="post" ...> とする必要があり、子要素の<button type="submit">のクリックによって発火します。その際、 onClick などのイベントハンドラー関数は必要としません。

uretanouretano

<Form> の子要素の input それぞれに name 要素を持たせることで、action 関数からそれぞれの要素にアクセスすることができるようになります。

<Form method="post">
    <input name="email" />
    <input name="adress" />
    ...
</Form>
export const action = async ({ request }: ActionFunctionArgs) => {
  const formData = await request.formData();
  const email = formData.get("email");
  const adress = formData.get("adress");  
};
uretanouretano

<Form action="edit"> をクリックすることで /contacts/${contactId}/edit に画面遷移する。

contacts.$contactId.tsx
<Form action="edit">
    <button type="submit">Edit</button>
</Form>
uretanouretano

データの削除

<Form action="XXX"> でのレンダリング先に対応するファイルにコンポーネントがなく、loader, action 関数だけがある場合、その関数を実行する。

contacts.$contactId.destroy.tsx
export const action = async ({
  params,
}: ActionFunctionArgs) => {
  invariant(params.contactId, "Missing contactId param");
  await deleteContact(params.contactId);
  return redirect("/");
};
contacts.$contactId.tsx
<Form
    action="destroy"
    method="post"
>
    <button type="submit">Delete</button>
</Form>

クリック時に対象のユーザーのcontactを削除する。

uretanouretano

contacts.$contactId.destroy.tsx が使用がわから import されていなくて、使用側からしたらどこに実行する関数が存在するのか不透明でわかりずらそう。

uretanouretano

loaderとaction

サーバー側で実行される関数。関数内のreturnで返した値がComponent内では、useLoaderDataの返り値として取得できる。
routesディレクトリ内でしか実行できない。

uretanouretano

loaderはコンポーネントのレンダリング時に実行する。
actionはFormタグ、inputタグ、Buttonタグを使ってデータを送信し、actionでそのデータを受け取って実行する。

uretanouretano

所感

  • ルーティングがシンプルだがファイル名が長くなってしまう
url path
/items routes/items.tsx
/items/${itemId} routes/items.$itemId.tsx