🥣

Remix 入門

2021/01/31に公開

こんにちは。

今日は React Router の製作者が新たに作った React のフレームワーク、 Remix を紹介したいと思います。

(現段階ではサポーターズプレビューライセンスとなっており、ライセンスを購入しない限り使えません。なんでオープンソースじゃないねんって思うかもしれませんが、よく考えると理に適ってるんですよね。オープンにしても誰かスポンサーしてくれるとは限らないし、フルタイムで作ってる。逆に本当にいいものを作ろうとしてる感じがでてすごくいい。オープンソースに甘やかされ過ぎなのかも。というのはまた別の話)

Remix と Next.js は多少重なっていますが、これらの異なる点があると思ってます。

SSG という概念がそもそもない

Next.js には色んなビルド法があって、その中でも最近注目を浴びてるのが SSG だと思います。ビルドタイムにプリレンダリングして、static file として CDN にアップする、というやつですね。ですが、Remix にはそれがありません。その代わりに、すでにある Fetch API を最大限使い Cache control で同じようなことを成し遂げてるんですよね。

なぜこのような方針に至ったかというと、まぁそもそもこれがメインで作られているからですね。

こちらのオフィシャルブログでも書かれているように、

majority of projects we've seen from our clients at React Training (the company we’ve run together for the last five years), have highly dynamic data that can't be captured at build time, so we want to build a framework specifically for them

数々のプロダクトのコンサルティングに関わってきたが、大多数はビルドタイムでは網羅できないすごくダイナミックなデータを扱っており、それを解決するフレームワークを作りたかった

だそうです。動的ルートが少なく全てプリレンダリングできるブログだったりするとこの問題にぶち当たることはないですが、何千もの動的ルートを持つECサイトだとそれなりに問題が出てきますよね。(例えば、staticPropsでrevalidateを設定していても多少は遅延が出たり、全てのルートを同じ revalidate にはしたくなかったり、、)

簡単にいうと、ISR と SSG は便利ですが万能ではない。

それの解決方法として、SSG は見捨てたということです。

Nested routes

Nested Routes の概念は Remix を触るにあたって非常に重要です。

基本的には、Next.js のファイルルーティングシステムと同じです。

ですが、Remix はそのファイル構成をみて、「今何が表示されていて」「次に何が表示されるか」を事前に知ることができ、それを用いて次のページのデータ、CSSやモジュールを fetch したり、ページの一箇所だけを変えたりすることができます。

例えば、以下のような画面を作るとします:

ルートというとコンポーネント:一つのルートというマッピングに慣れていると思いますが、Remix の場合だと上のようなネストされたルーティングも可能となります。それに加え、Remix は全てのルートのネスト状況を描画する前に理解するので、例えば上の画面で Details や Activity をクリックしても、変更が加わったところだけデータを fetch するので、それをラップしてる親コンポーネントには何も影響を及ぼしません。

これを Next.js でやろうとすると、グローバル _app.js などでレイアウトのステートを保持させたり、いろいろハッキーなことをしないとできないですよね。レイアウトの数が増えたりしたときにはもう、、、🙃

SSRルートのデータコントロール

Remix ではそれぞれのファイルに loader, headers, meta というのを指定することができます。loader は Next.js でいう getServerSideProps みたいなやつですね。ですが、少し違うのはレスポンス時の挙動を headers や metaで完全にコントロールできるというところです。例えば、以下のようなコードがあるとします:

// app/routes/gists.tsx

import { json } from '@remix-run/data'

export function loader() {
  const post = await getPost()

  const oneDay = 86400
  
  const secondsSincePublished =
    (new Date().getTime() - post.frontmatter.published) / 1000
    
  const barelyPublished = secondsSincePublished < oneDay

  const maxAge = barelyPublished ? 60 : oneDay * 7

  const swr = oneDay * 30

  return json(post, {
    headers: {
      'cache-control': `public, max-age=${maxAge}, stale-while-revalidate=${swr}`,
    },
  })
}

export funtion headers({ loaderHeaders }) {
   return {
      "cache-control": loaderHeaders.get('cache-control')
   }
}

export function meta({data: post}: {data: Post}) {
  return {
    title: post.frontmatter.title,
  }
}

このように、ページごとに Cache control をデータとともに送ることができます。loader で header の値などを返すと、headers の引数として受け取ることができて、そいつを用いて動的ページごとにカスタマイズされた cache-header を付けたりできます。上のコードの場合だと、最新のブログポストだけキャッシュタイムを短く設定し、著者に変更をすぐ反映したりみたいなことをしています。

エラーハンドリング

Remix ではページごとに ErrorBoundary を設定することができます。

例えば、以下のコードがあるとします。

export function ErrorBoundary({ error }) {
   return <div>Error occurred. {error.message}</div>
}

export default function Gists() {
  throw new Error('ERRROR')
  return (
    <div>
      <h2>Public gists</h2>
    </div>
  );
}

特殊な ErrorBoundary 関数をエキスポートすると、Gists コンポーネント内で起こったエラーは Error Boundary にキャッチされて、適切なエラー表示ができます。ちなみに、これは loaders 内で起こったエラーも対処できます。

export function ErrorBoundary({ error }) {
   return <div>Error occurred. {error.message}</div>
}

export function loader() {
   throw new Error('Error occurred')
}

export default function Gists({ gists }) {
  return (
    <div>
      <h2>Public gists</h2>
      {gists.map((gist) => (
        <div>{gist}</div>
      ))}
    </div>
  );
}

これで、コンポーネント内でエラーチェックで表示変更、など変なコードを付け加える必要もないですね。

完全にプラガブルなフレームワーク

Remix は http リクエストハンドラと静的アセットだけ提供しているので、完全なウェブサーバーフレームワークではありません。なので、柔軟に既存プロジェクトなどに導入できます。例えば、Expressの場合だと、

// server.js

const express = require("express");
const { createRequestHandler } = require("@remix-run/express");

let app = express();

app.all(
  "*",
  createRequestHandler({
    // Uncomment the following line if you don't want sessions. This will
    // disable the warning message when no session middleware is present.
    //enableSessions: false,
    getLoadContext() {
      // Whatever you return here will be passed as `context` to your loaders
      // and actions.
    },
  })
);

で設定完了です。あとはプロセスを走らせるために静的アセットをビルド・サーバーをウォッチ(node server.js)するだけ。サーバーのエコシステムはなんでもいいので、導入しやすいですね。

以上が Remix を一週間ほど触った個人的な感想でした。まだまだ開発段階なので、これからも色んな機能が追加されていくと思いますので、随時アップデートしていきます。

Discussion