🐾

Remix 勉強日記:チュートリアルのその前に

2023/11/17に公開

はじめに

フロントエンドで Remix が使いたいなと思ったので勉強している。フロントエンドはド素人。

以下のチュートリアルはやってみたが諸々よく分からんと思ったので調べたことをまとめておく。

https://remix.run/docs/en/main/start/tutorial

なんやねんその文法: JSX

JavaScript はかじったことがあるのだが、React はよく分からん文法で書かれている。チュートリアルapp/root.tsx から引用する。

app/root.tsx
export default function App() {
  return (
    <html lang="en">
      {/* other elements */}
      <body>
        <div id="sidebar">{/* other elements */}</div>
        <div id="detail">
          <Outlet />
        </div>
        {/* other elements */}
      </body>
    </html>
  );
}

return が HTML を返してはる……! これは JSX(JavaScript XML) という拡張文法であり、その TypeScript 版が TSX である。

なんの説明もなく出てくるあたり、Remix のチュートリアルはこの JSX を理解している前提で書かれている。ではその JSX はどこで勉強するのかといえば、以下の React の公式教材である。

https://ja.react.dev/learn/describing-the-ui

こちらをやってからでないと Remix のチュートリアルは手順通りにコードを切り貼りさせられている感じで、ものはできるが、何をやっているのか、何が起きているのか正直まったく分からない。

なにその CSS の読み込み方

これも Remix のチュートリアルから引用。

import type { LinksFunction } from "@remix-run/node";
// existing imports

import appStylesHref from "./app.css";

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

んんん?? import appStylesHref from "./app.css";???

CSS をインポートするというのは意味が分からないのだが、これは Remix のドキュメントで Regular stylesheet imports と呼ばれている正規の CSS インポート手段である。

https://remix.run/docs/en/main/styling/css

CSS のインポート方法には他にも以下の3種類があり、これらを用いた場合は CSS Bundle の機能によりひとつの CSS ファイルにまとめられるらしい。

https://remix.run/docs/en/main/styling/bundling

使い分けはよく分かっていないが上記3つのうち Vanilla Extract 以外は構文が若干キモい。

CSS をインライン的にコード中に書くなら Vanilla Extract で書いておいて、既存の CSS ファイルやルーティングごとなど異なるタイミングで追加で CSS を読み込ませたい場合には Regular インポートを使うという感じだろうか。

CSS Bundle の読み込みに対応するコードは以下で、npx create-remix@latest でプロジェクトを作成した場合、デフォルトで app/root.tsx に書かれている。

app/root.tsx
import { cssBundleHref } from "@remix-run/css-bundle";
import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno

export const links: LinksFunction = () => [
  ...(cssBundleHref
    ? [{ rel: "stylesheet", href: cssBundleHref }]
    : []),
  // ...
];

一方で、Regular インポートに対応するインポートのスニペットは以下。

import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno

import styles from "~/styles/dashboard.css";

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

どうやら import styles from ...styles の部分には任意の名前をつけてよいらしい。from が付いているので紛らわしいが、CSS ファイルから何かをインポートするというよりは、CSS ファイルそのものへの参照に styles という名前を付けていると解釈したほうがよさそうである。

どういった形式のオブジェクトで読まれているのか若干気になるのであとで暇があったら確認してみようと思う。

オォゥ、ドッカラ イジレバ イインデスカ?

npx create-remix@latest で生成した app/root.tsx は大体以下のような感じである。

app/root.tsx
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>
        <Outlet />
        <ScrollRestoration />
        <Scripts />
        <LiveReload />
      </body>
    </html>
  );
}

それなのにビルドしてサーバー立ててアクセスすると以下のような Web サイトが表示される。

チュートリアルを読んでいけば分かるがこれはルーティングが自動的に行われているからである。ページに表示されている「Welcome to Remix」などの部分は app/route/_index.tsx の中に書かれているコンポーネントが app/root.tsx<Outlet /> の部分に埋め込まれたものである。

app/route/_index.tsx
...

export default function Index() {
  return (
    <div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}>
      <h1>Welcome to Remix</h1>
      <ul>
        <li>
          <a
            target="_blank"
            href="https://remix.run/tutorials/blog"
            rel="noreferrer"
          >
            15m Quickstart Blog Tutorial
          </a>
        </li>
	...

"outlet" という単語は日本語で生活しているとアウトレットモールのイメージが強すぎてニュアンスが理解しにくいと思うが「"out"(外に出る)-"let"(状態にする)[1]」という意味であり「出口」のことを指す。身近なものでいえば電気の取出口である家庭用コンセントは "outlet" である。

話が逸れたが、この app/route/_index.tsx を編集してやればインデックスページを編集できるし、どんなルート(route)においても表示されていてほしいメニューバーやサイドバーなどがあれば app/root.tsx に書けばよい。

app/root.tsx<Outlet /> 以外にもいろいろ書かれているのでいじるのが怖いが、ドキュメントの Components のところに役割が書かれているのでそれをチェックするとよい。簡単にまとめておくと以下である。

コンポーネント 機能
<Meta /> 冒頭の <meta> タグ相当。
<Links /> 冒頭の <links> タグ相当。これを消すと CSS Bundle などが読み込まれないはず。
<Outlet /> ルーティングで埋め込まれるコンポーネント。ルーティングしないなら消してもよい。
<ScrollRestoration /> 再レンダー時やドメインなどをまたいだときにスクロール位置を復元する機能。消さないほうが吉。
<Scripts /> クライアントのランタイムを挿入する。消すと JavaScript が動かなくなるから消しちゃダメ。
<LiveReload /> 開発環境でのライブリロードを有効にする。製品環境では自動的に消えるので書いておいてよい。

共通コンポーネントや CSS ファイルはどこに置く?

ChatGPT4 に聞いたところ以下のような回答を得た。

  • 複数のルート(route)で使うようなコンポーネントは app/components などに配置する
  • CSS ファイルは app/styles などに配置する

界隈でどういう慣習になっているかは知らないが、妥当だと思う。

というわけで遊んでみよう!

npx create-remix@latest で生成したプロジェクトで以下のようにファイルを編集。

app/route/_index.tsx
import type { MetaFunction } from "@remix-run/node";
import type { LinksFunction } from "@remix-run/node";

import styles from "~/styles/index.css";

export const meta: MetaFunction = () => {
  return [
    { title: "The Hell" },
    { name: "description", content: "Welcome to the Hell!" },
  ];
};

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

export default function Index() {
  return (
    <div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}>
      <h1>Welcome to the Hell...</h1>
    </div>
  );
}
app/styles/index.css
@import url('https://fonts.googleapis.com/css2?family=Creepster&display=swap');

body {
    background-color: #282828; /* 背景色を暗いグレーに設定 */
    color: #ffffff; /* テキストの色を白に設定 */
}

h1 {
    font-family: 'Creepster', cursive; /* おどろおどろしいフォントファミリー */
    color: #ff0000; /* 色を赤に設定 */
    text-shadow: 3px 3px 2px #000000; /* テキストに影をつける */
    font-size: 64px; /* フォントサイズを大きくする */
    margin-left: 20px; /* 左側にマージンを設定 */
}

んん、いい感じだ。

おしまい

↑ のジョークの解説(読むなんて無粋なマネはしてくれるなよ)
  • 「我々 IT 土方に休息など存在しない」
  • 「REST なサービスなんて存在しない」

  • 「唯一抜きん出て並ぶ者なし(Eclipse first, the rest nowhere.)」

にかけたトリプルミーニングです。

脚注
  1. 特に日本の英語教育では "let" の意味を慣用表現でしか習わずニュアンスがつかみづらいと思うのでもう少し例を挙げておくと、"Let's go." つまり "Let us go." は「私たちを『目的地に向かう』の状態にする」というニュアンスで「一緒に行こう」になる。"Let it go." は「それがいま存在している状態を維持してやろう」で「放っておいてやろう/あるがままにしておこう」という意味になる。"Let me see." は「私を『見る』の状態にさせてくれ」で「ちょい見してみ」みたいな意味になる。JavaScript の let x = 1; という文法は数学の用語で前提を述べるときに "Let x be real."(x は実数とする。)などと用いられる慣用表現が関数型言語を経由して JavaScript に伝わったものである。 ↩︎

Discussion