モジュール制約
Remix はサーバーとブラウザの両方でアプリケーションを実行するために、アプリケーションモジュールとサードパーティ依存関係はモジュール副作用に注意する必要があります。
- サーバー専用コード - Remix はサーバー専用コードを削除しますが、サーバー専用コードを使用するモジュール副作用がある場合は削除できません。
- ブラウザ専用コード - Remix はサーバーでレンダリングされるため、モジュールはモジュール副作用を持つことはできませんし、ブラウザ専用 API を呼び出す最初のレンダリングロジックを持つこともできません。
サーバーコードの刈り込み
Remix コンパイラは、ブラウザバンドルからサーバーコードを自動的に削除します。私たちの戦略は実際には非常に単純ですが、いくつかのルールに従う必要があります。
- それはあなたのルートモジュールの前に「プロキシ」モジュールを作成します
- プロキシモジュールはブラウザ固有のエクスポートのみをインポートします
loader
、meta
、コンポーネントをエクスポートするルートモジュールを考えてみましょう。
サーバーはこのファイルのすべてを必要としますが、ブラウザに必要なのはコンポーネントと meta
だけです。実際には、prisma
モジュールをブラウザバンドルに含めると、完全に壊れてしまいます。そのモジュールは node 専用の API でいっぱいです!
ブラウザバンドルからサーバーコードを削除するために、Remix コンパイラはあなたのルートの前にプロキシモジュールを作成し、代わりにそれをバンドルします。このルートのプロキシは次のようになります。
コンパイラは app/routes/posts.tsx
のコードを分析し、meta
とコンポーネント内のコードのみを保持します。その結果、次のようになります。
かなりきれいです!これで、ブラウザ用に安全にバンドルできます。では、何が問題なのでしょうか?
モジュール副作用なし
副作用に慣れていない場合は、あなただけではありません!今すぐ副作用を特定するお手伝いをします。
簡単に言うと、副作用とは、_何かをする_可能性のあるコードです。モジュール副作用とは、_モジュールがロードされたときに何かをする_可能性のあるコードです。
モジュール副作用とは、モジュールをインポートするだけで実行されるコードのことです。
先ほどのコードを例に、コンパイラが使用されていないエクスポートとそのインポートを削除できる様子を見てきました。しかし、この一見無害なコード行を追加すると、アプリが壊れてしまいます!
console.log
は_何かをしています_。モジュールはインポートされ、すぐにコンソールに出力されます。コンパイラは、モジュールがインポートされたときに実行する必要があるため、これを削除しません。コンパイラはこのようなものをバンドルします。
ローダーはなくなりましたが、prisma 依存関係は残っています!もし私たちが console.log("hello!")
のような無害なものをログに出力していたら、問題ありませんでした。しかし、prisma
モジュールをログに出力したので、ブラウザはそれを処理するのに苦労するでしょう。
これを修正するには、コードを_ローダーの中に_移動することで、副作用を削除します。
これはもはやモジュール副作用(モジュールがインポートされたときに実行されます)ではなく、ローダーの副作用(ローダーが呼び出されたときに実行されます)。コンパイラは、このモジュール内の他の場所で使用されていないため、ローダーと prisma のインポートの両方を削除します。
時折、ビルドがサーバーで実行されるべきコードをツリーシェイクする際に問題が発生することがあります。このような場合は、ファイルの拡張子に .server
を追加してファイルタイプを指定する、という慣習を使用することができます。たとえば、db.server.ts
のようになります。ファイル名に .server
を追加することで、コンパイラはブラウザ用のバンドルを作成する際にこのモジュールやそのインポートを気にしないようにします。
高階関数
Remix の初心者の中には、ローダーを「高階関数」で抽象化しようとする人もいます。このようなものになります。
そして、このように使用しようとします。
おそらく、これがモジュール副作用であるため、コンパイラは removeTrailingSlash
コードを刈り取ることができないことに気づいているでしょう。
このタイプの抽象化は、応答を早期に返すために導入されます。loader
で Response
をスローできるため、これをよりシンプルにすることができ、同時にモジュール副作用を削除することで、サーバーコードを刈り取ることができます。
そして、このように使用します。
これは、このようなものがたくさんある場合にも、はるかに読みやすくなります。
もしあなたが少し余分な読書をしたいのであれば、「push vs. pull API」について調べてみてください。応答をスローする能力は、モデルを「push」から「pull」に変更します。これは、人々がコールバックよりも async/await を好み、高階コンポーネントやレンダリングプロップよりも React フックを好むのと同じ理由です。
サーバーでのブラウザ専用コード
ブラウザバンドルとは異なり、Remix はサーバーバンドルから_ブラウザ専用コード_を削除しようとはしません。なぜなら、ルートモジュールはサーバーでレンダリングするためにすべてのエクスポートを必要とするからです。つまり、ブラウザでのみ実行されるべきコードに注意することがあなたの仕事になります。
これはアプリを壊します。
モジュールスコープで window
にアクセスしたり、API を初期化したりするような、ブラウザ専用モジュール副作用を避ける必要があります。
ブラウザ専用 API の初期化
最も一般的なシナリオは、モジュールがインポートされたときにサードパーティ API を初期化することです。これに対処する簡単な方法がいくつかあります。
ドキュメントガード
これは、document
が存在する場合にのみライブラリが初期化されることを保証します。つまり、ブラウザにいるということです。Deno のようなサーバーランタイムはグローバルな window
を利用できるため、window
ではなく document
をお勧めします。
遅延初期化
この戦略は、ライブラリが実際に使用されるまで初期化を延期します。
ライブラリを複数回初期化することを避けたい場合は、モジュールスコープ変数に格納することができます。
これらの戦略のいずれも、ブラウザモジュールをサーバーバンドルから削除するわけではありませんが、API はイベントハンドラやエフェクト内でのみ呼び出されるため、問題ありません。イベントハンドラやエフェクトはモジュール副作用ではありません。
ブラウザ専用 API を使用したレンダリング
もう1つの一般的なケースは、レンダリング中にブラウザ専用 API を呼び出すコードです。React(Remix だけではありません)でサーバーレンダリングを行う場合、API はサーバーには存在しないため、これは避ける必要があります。
これは、サーバーがローカルストレージを使用しようとするため、アプリを壊します。
これは、コードを useEffect
に移動することで修正できます。useEffect
はブラウザでのみ実行されます。
これで、localStorage
は最初のレンダリング時にアクセスされなくなりました。これはサーバーで動作します。ブラウザでは、その状態はハイドレーション後にすぐに埋められます。しかし、大きなコンテンツレイアウトシフトが発生しないことを願っています!もし発生したら、その状態をデータベースやクッキーに移してみましょう。そうすれば、サーバーサイドからアクセスできます。
useLayoutEffect
このフックを使用すると、React はサーバーでそれを使用していることを警告します。
このフックは、次のような状況で状態を設定する場合に適しています。
- 要素がポップアップしたときの要素の位置(メニューボタンなど)
- ユーザーの操作に応じてスクロール位置
つまり、エフェクトをブラウザのペイントと同じタイミングで実行することで、ポップアップが 0,0
に表示されてからバウンドして正しい位置に移動するのを防ぐことができます。レイアウトエフェクトを使用することで、ペイントとエフェクトを同時に実行し、このようなちらつきを防ぐことができます。
これは、要素内でレンダリングされる状態を設定する場合には適していません。useLayoutEffect
で設定された状態を要素内で使用していないことを確認すれば、React の警告は無視できます。
useLayoutEffect
を正しく呼び出していることがわかっているだけで、警告を無効にする必要がある場合は、サーバーでは何も呼び出さない独自のフックを作成するのが一般的な解決策です。useLayoutEffect
はブラウザでのみ実行されるため、これは有効です。この方法を使用する際は十分に注意してください。この警告は、正当な理由で表示されているのです!
サードパーティモジュール副作用
一部のサードパーティライブラリには、React のサーバーレンダリングと互換性のない独自のモジュール副作用があります。通常は、機能検出のために window
にアクセスしようとしています。
これらのライブラリは、React のサーバーレンダリングと互換性がないため、Remix とも互換性がないことを意味します。幸いなことに、React エコシステムにおけるサードパーティライブラリのほとんどは、このような動作はしません。
代替手段を見つけることをお勧めします。しかし、どうしても見つからない場合は、patch-package を使用してアプリで修正することをお勧めします。