🥜

Remixでライブラリを使う前に気をつけること

2022/02/11に公開

はじめに

私はRemixでSupabaseを使おうとして時間を10時間ほど溶かしたので、ここでRemixの作法を学習しておきます。そもそも、Remixはコードをサーバー側とブラウザ側に分けているわけです。具体的に言うと、Remixはサーバー用のファイルとブラウザ用のファイルを生成していることになります。Remixだけならそれほど難しくないと思いますが、ライブラリを使用すると厄介になることがあります。ここからは公式を元にやっちゃいけないことを書きます。
https://remix.run/docs/en/main/guides/constraints

用語

用語の説明からさせてください。

  • Server-only code サーバー上でのみ実行される予定のバンドル前のコード
  • Browser-only code ブラウザ上でのみ実行される予定のバンドル前のコード
  • Server bundle  Remixがまとめたサーバー上で実行されるコード
  • Browser bundle  Remixがまとめたブラウザ上で実行されるコード

大事なこと

  • Remixはサーバー上でレンダリングします。
  • RemixはBrowser bundleを生成する際、不要なServer-only codeを削除(刈り込み)します。
  • RemixはServer bundleを生成する際、刈り込みをしません。

Sever-only code

RemixはBrowser bundleを生成する際にServer-only Codeを刈り込みします。しかし、刈り込みされないケースがあります。

Module Side Effects

たとえば、次のようなモジュールを考えてみます。

import { prisma } from "../db";
console.log(prisma);

prismaはサーバー側、console.logはブラウザ側です。この場合、console.logに引っ張られてブラウザ側にprismaが残ってしまいます。このことをRemixはModule Side Effects(モジュールの副作用)と言っています。

モジュールの副作用とは、モジュールをインポートするだけで実行されるコードのことです。
https://remix.run/docs/en/main/guides/constraints#no-module-side-effects

これを回避するにはloaderの中にconsole.logを入れる必要があります。副作用というのは、この場合I/O実行を指しているのだと思われます。

import { prisma } from "../db";
export async function loader() {
  console.log(prisma);
  return prisma.post.findMany();
}

この他にもライブラリによってはRemixコンパイラがコードの切り分けを間違えちゃうことがたまにあるみたいです。

時折、ビルドはServer-only codeをツリーシェイクすることがあります。

その場合は、以下の通り対応します。

ファイルタイプの前に拡張子.serverを付けて、例えばdb.server.tsのようにファイル名を付けます。ファイル名に .server を追加することで、ブラウザ用にバンドルする際に、このモジュールやそのインポートを気にしないようにコンパイラにヒントを与えます。

これは便利ではあるのですが、うっかり付けないように気をつけないといけません。.serverを付けるのはServer-only codeだけです。
.clientと付けた場合はどうなるんでしょうか? 試した限りではApplication Errorが発生しました。なぜなんでしょう。とにかくRemixでは考えなしに.clientや.serverをファイル名につけない方が良さそうです。

Browser-only code

Browser bundleのServer-only codeはRemixが大体刈り込みしてくれますが、Server bundleのBrowser-only codeについては刈り込んでくれません。庭師だって家の中の主人の髪までは切ってくれない。そんな話でしょうか。ともかく、Remixはサーバー上でレンダリングするので、Browser-only codeの実行タイミングに気をつける必要があります。

自分で書いたコードに問題あり

Firebaseを初期化するコードですが、これではうまくいきません。

ダメな例
import firebase from "firebase/app";

firebase.initializeApp(document.ENV.firebase);

ガード、もしくは遅延初期化(Lazy initialization)が必要になります。

良い例
import firebase from "firebase/app";

if (typeof document !== "undefined") {
  firebase.initializeApp(document.ENV.firebase);
}

こういうときって普通documentじゃなくてwindowを使うんじゃない?って思うんですが、Dinoなどサーバーによってはglobalなwindowを持ってるパターンがあるらしいです。
また、ブラウザ上にしかないものをサーバー上でレンダリングするのもダメです。

ダメな例
function useLocalStorage(key) {
  const [state, setState] = useState(
    localStorage.getItem(key)
  );

  const setWithLocalStorage = nextState => {
    setState(nextState);
  };

  return [state, setWithLocalStorage];
}

ブラウザでlocalStorageを実行するようにタイミングを変更します。

良い例
function useLocalStorage(key) {
  const [state, setState] = useState(null);

  useEffect(() => {
    setState(localStorage.getItem(key));
  }, [key]);

  const setWithLocalStorage = nextState => {
    setState(nextState);
  };

  return [state, setWithLocalStorage];
}

ライブラリに問題あり

ライブラリの内部でwindowに直接アクセスしようとしている場合ももちろん使えません。Reactのエコシステムに組み込まれているものならReactの作法に従っているので大丈夫とのことです。ですが、そのほかのライブラリの場合、代替できるものを探す必要があります。
Issueにも挙がっているようです。
https://github.com/remix-run/remix/issues/1618
どうしても使いたい場合は、ライブラリにpatchをあてることでwindowに直接アクセスするコードを書き換えることができます。
https://github.com/ds300/patch-package

終わりに

ほとんどRemixの公式の和訳となってしまいましたが、追記があればこちらに書く予定です。現場からは以上です。

Discussion