Remixでライブラリを使う前に気をつけること
はじめに
私はRemixでSupabaseを使おうとして時間を10時間ほど溶かしたので、ここでRemixの作法を学習しておきます。そもそも、Remixはコードをサーバー側とブラウザ側に分けているわけです。具体的に言うと、Remixはサーバー用のファイルとブラウザ用のファイルを生成していることになります。Remixだけならそれほど難しくないと思いますが、ライブラリを使用すると厄介になることがあります。ここからは公式を元にやっちゃいけないことを書きます。
用語
用語の説明からさせてください。
- 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にも挙がっているようです。
どうしても使いたい場合は、ライブラリにpatchをあてることでwindowに直接アクセスするコードを書き換えることができます。
ブラウザでのエラー
以下ではブラウザで出たエラーに関する対処法を挙げます。
- Uncaught Error: Hydration failed because the initial UI does not match what was rendered on the server.
Server bundleとBrowser bundleとでレンダリング直後のUIに差異がある際に生じるエラーです。これは、サーバーでレンダリングはしたけれどブラウザ側でそのレンダリングの一部が無視されていることを示します。
Chrome拡張機能の影響でエラーが出ていないかチェックしてみてください。それでも直らない場合、RemixのDiscussionも参照してみてください。
終わりに
ほとんどRemixの公式の和訳となってしまいましたが、追記があればこちらに書く予定です。現場からは以上です。
Discussion