remix触ってみる
remix
Web標準に準拠することを重視した設計されている。
例
- データを取得するときはFetch API
- データミューテーションを行うときはFormタグ
- サーバーにデータを持たせるときはクッキーとセッション
Tutorial を触ってみる
setup
$ npx create-remix@latest --template remix-run/remix/templates/remix-tutorial
$ cd my-remix-app
$ yarn install
テンプレートで生成されたファイルを見てみる
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 タグ が含まれている
app/data.ts
tutorial アプリで使用するデータ
//
"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
が使われている。
スタイルを設定する
プレーンな CSS ファイルを JavaScript モジュールに直接 import 可能。
以下を import し、追記すると CSS を読みこみスタイルが当てられる。
+ import type { LinksFunction } from "@remix-run/node";
+ import appStylesHref from "./app.css?url";
+ export const links: LinksFunction = () => [
+ { rel: "stylesheet", href: appStylesHref },
+ ];
ルーティング
ファイルベースルーティングが可能なので、app/routes 配下にファイルを作成する。
$ mkdir app/routes
$ touch app/routes/contacts.\$contactId.tsx
remix ではファイル名の.
でURLを分割する。
app/routes/contacts.$contactId.tsx
の場合 URL は contacts/123
, contacts/abc
といった動的パラメータに対応したページへアクセス可能となる。
子ルートを親レイアウト内でレンダリングするには、親内で をレンダリングする必要があるので、root.tsx
に <Outlet />
を追加する。
+ 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>
);
}
データフェッチング
loder
という非同期関数を作成し、コンポーネント内でuseLoaderData
を使って呼び出します。
・・・
export const loader = async () => {
const contacts = await getContacts();
return json({ contacts });
};
export default function App() {
const { contacts } = useLoaderData<typeof loader>();
return ( ・・・ )
}
getContacts
から取得した contacts を useLoader 経由でコンポーネント内で使えるようになります。
ローダーでのURL params
contacts/contactId で URL パラメータから params を取得して、 loader からデータを取得するには、
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 ( ・・・ )
}
データの作成
action
を作成する。
+ 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
が発火する。
データの更新
編集ページを作成する
$ touch app/routes/contacts.\$contactId_.edit.tsx
URL をネストしたいが、自動レイアウト ネストは不要という場合、親セグメントの末尾にアンダースコアを付けるとネストをオプトアウトできる。
/contacts/${contactId}/edit
で対象のユーザのデータを更新するページへアクセスできる。
action
関数を作成する。
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
などのイベントハンドラー関数は必要としません。
<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");
};
<Form action="edit">
をクリックすることで /contacts/${contactId}/edit
に画面遷移する。
<Form action="edit">
<button type="submit">Edit</button>
</Form>
データの削除
<Form action="XXX">
でのレンダリング先に対応するファイルにコンポーネントがなく、loader
, action
関数だけがある場合、その関数を実行する。
export const action = async ({
params,
}: ActionFunctionArgs) => {
invariant(params.contactId, "Missing contactId param");
await deleteContact(params.contactId);
return redirect("/");
};
<Form
action="destroy"
method="post"
>
<button type="submit">Delete</button>
</Form>
クリック時に対象のユーザーのcontactを削除する。
contacts.$contactId.destroy.tsx
が使用がわから import されていなくて、使用側からしたらどこに実行する関数が存在するのか不透明でわかりずらそう。
loaderとaction
サーバー側で実行される関数。関数内のreturnで返した値がComponent内では、useLoaderData
の返り値として取得できる。
routes
ディレクトリ内でしか実行できない。