svg.js を使って DOM をグリグリ触るコンポーネントを作り、それを Next.js 環境で動かしてみたところ、エラーが発生。そこから脱出するまでに時間がかかりました。
その時のお話をしたいと思います。
作っていたもの
svg.js というライブラリを使って SVG で画像を組み立てるものを作っていました。
DOM を操作するので、"use client"
指定をして、対象のコンポーネントはクライアントコンポーネントとなるようにしました。
発生したエラー
Cannot read property 'createElementNS' of undefined
このエラーがサーバー側で発生しました。
"クライアントコンポーネントなのになぜサーバー側でエラーが発生するんだ?"
発生した理由
Next.js のドキュメントに次の記述があります。
最初のページロードを最適化するために、Next.jsはReactのAPIを使用して、クライアントコンポーネントとサーバーコンポーネントの両方について、サーバー上に静的なHTMLプレビューをレンダリングします。つまり、ユーザーがアプリケーションに最初にアクセスしたとき、クライアントがクライアント コンポーネントのJavaScriptバンドルをダウンロード、解析、実行するのを待つことなく、すぐにページのコンテンツが表示されます。
--- DeepLによる翻訳
つまりクライアントコンポーネントとはいえ、サーバー側でもレンダリングされるのです。
そしてその中にクライアント側に依存するコードがある場合は、サーバー側でエラーが発生してしまうのです。
このことを教えてくれたのはstack overflowでした
この記事の場合は localStorage が問題でしたが、僕の場合は DOM 操作が問題だったようです。
解決方法
先程の記事では、こういったエラーを起こさないようにするには、クライアント側特有の操作をするコードを、useEffect
の中に置くと良い、ということでしたので svg.js の Svg
オブジェクトを生成する部分をuseEffect
に移動したらたしかにその部分で発生していたエラーは出なくなりました。
しかし他の部分では、useEffect
内に移動すると別な理由でうまく動作しないところがあり、再び詰まってしまいました。
"サーバー側でややこしいことせんでええから、純粋にクライアント側だけで動かす方法ないんか?"
とぼやいていたら、
ありました。
最終的な解決方法 Lazy Loading
Next.js の Lazy Loading で、コンポーネントを遅延ロードすることができますが、その際に {ssr: false}
のオプションを渡してあげると、純粋なクライアント側コードとして扱われます。つまりサーバー側ではレンダリングされません。
import Editor from './Editor';
としていた部分を
import dynamic from 'next/dynamic';
const Editor = dynamic(() => import('./Editor'), { ssr: false });
という風に書き換えると、Editor
コンポーネントは遅延ロードされ、サーバー側ではレンダリングされなくなります。これですべてのコンポーネントで、エラーが発生することはなくなりました。
このことを教えてくれたのは、"Common Errors in Next.js and How to Resolve Them" というブログ記事でした。
このエラーを解決するにはさまざまなアプローチがありますが、単純な選択肢のひとつは、ブラウザのウィンドウ・オブジェクトを必要とするコード・ブロックを実行するためにreactの
useEffect()
フックを使用し、ページ・コンポーネントがマウントされたときだけコードが実行されるようにすることです。もうひとつの方法は、ブラウザのウィンドウを必要とするコード部分をスタンドアロンコンポーネントに変換し、Next.jsのダイナミックインポート機能を使ってページコンポーネントにインポートすることです。Next.jsのダイナミックインポートは、コンポーネントを遅延ロードまたはオンデマンドで動的にロードするための機能です。ただし、この機能を使用するときに、サーバーレンダリングを有効または無効にできる追加のssrオプションが含まれています。
ssr値をfalseに設定するだけで、ブラウザの
window
やdocument
に依存するコンポーネントや外部パッケージをロードできるようになります。--- DeepLによる翻訳
わかれば簡単なことでしたが、最終的な解決法見つけるまでに時間がかかってしまいましたので、他の方の参考になるかと思い記事にしました。
追記: MUI を使っているなら NoSsr コンポーネント
MUI を使っているなら、<NoSsr>
コンポーネントでラップしてあげるだけで、サーバー側でのプリレンダリングは行われなくなります。