LoginSignup
145
54
お題は不問!Qiita Engineer Festa 2023で記事投稿!

7歳娘「パパ、ReactのHydration Errorってなんで起こるの?」

Last updated at Posted at 2023-07-02

クライアントコンポーネントのお話です。
(サーバーコンポーネントは出て来ません)

ある日の我が家

娘「ねぇ、パパ?」

ワイ「なんや、娘ちゃん?」

娘「ハイドって何?」

ワイ「おお、今時の子供はハイドさんのことを知らんのか」
ワイ「L'Arc〜en〜Cielのボーカリストさんやがな」

娘「違う、そっちじゃなくて」
娘「Reactのハイドレーションのほう」

ワイ「ファッ!?」
ワイ「ハイドレーションのことをハイドって呼んでんのかい」

娘「うん」
娘「Next.jsでWebアプリを作っていたら、こんなハイドレーションエラーっていうのが起こっちゃって」

画面スクショ

【和訳】
エラー: テキストの内容がサーバーでレンダリングされた HTML と一致しません。
警告: テキストコンテンツが一致しませんでした。サーバー: "6" クライアント "5"

ワイ「おお〜」

娘「それで、ハイドレーションって何なのかな?って思ったの」

ワイ「なるほどなぁ〜」
ワイ「どんなWebアプリを作ってたん?」

娘「サイコロアプリだよ」
娘「こんな感じ」

画面

画面スクショ

娘「しかもね」
娘「6の目が出た場合には」
娘「↓こう表示されるんだよ〜」

画面スクショ

ワイ「おー、ええやないか」

娘「えへへ」
娘「でも、作ってる途中で、さっきのハイドレーションエラーが出ちゃったの・・・」

ワイ「サイコロっていうランダムなものを扱ってるわけやもんなぁ」
ワイ「これは確かに、気をつけんとハイドレーションエラーが起きそうやな」

娘「で、そのハイドレーションて何なの?」

ハイドレーションとは

ワイ「ほな、ハイドレーションについて説明しようと思うんやけど」
ワイ「その前に、現状のコードがどんな感じなのか見せてもらえる?」

娘「うん」
娘「こんな感じだよ」

TSX(一部抜粋)
  // saikoro() という関数で、1〜6のランダムな数値を生成。
  // それを useState の初期値として設定。
  const [saikoroNumber, setSaikoroNumber] = useState<number>(saikoro())

  return(
    <>
      <h1>サイコロの目</h1>
      <p>{ saikoroNumber }</p>
      {/* 6の場合はメッセージを表示 */}
      {saikoroNumber === 6 && <p>やった!6だね!</p>}
      <button onClick={roll}>サイコロをふる</button>
    </>
  )

ワイ「なるほどな」
ワイ「なあ、娘ちゃん」
ワイ「このh1要素とかp要素とかbutton要素が」
ワイ「どの段階でHTMLに書かれるかは知ってるか?」

娘「えーとね」
娘「まずは、ユーザーさんがブラウザを使って、このWebサイトにアクセスするでしょ?」
娘「それで、サーバからHTMLが返ってきて」
娘「その段階では───」

<div id="__next"></div>

娘「↑こんな感じのHTMLが存在して」
娘「そこに、Reactがh1要素とかp要素とかbutton要素をレンダリングしてくれる感じでしょ?」
娘「そして───」

  <div id="__next">
    <h1>サイコロの目</h1>
    <p>5</p>
    <button>サイコロをふる</button>
  </div>

娘「↑こんなDOMができあがる」
娘「そういう順番じゃないの?」

ワイ「いや、違うんや」
ワイ「サーバからHTMLが返ってきた時点で───」

  <div id="__next">
    <h1>サイコロの目</h1>
    <p>5</p>
    <button>サイコロをふる</button>
  </div>

ワイ「↑こう、もう書かれてるんや」

娘「え、それはサーバサイドレンダリング(SSR)した場合じゃないの?」

ワイ「いや、SSRしてなくてもそうやで」
ワイ「Next.jsアプリをビルドすると、各ルート分のhtmlファイルが生成されるやん?」

娘「あ〜」
娘「index.htmlが1つだけ生成されるんじゃなくて」
娘「各ルート分、つまり子ページの分のファイルも生成されるんだね」

ワイ「せやで」
ワイ「その時点で、各ページのHTMLの中身は書かれてるんや1

娘「へぇ〜」
娘「そうなんだ」
娘「シングル・ページ・アプリケーションは、JavaScriptでHTMLをレンダリングするから」
娘「サーバから来た時点のHTMLは殆ど空っぽなのかと思ってた」

ワイ「ワイも昔はそう思ってたわ」
ワイ「でも、それだと───」

  1. サーバから、ほぼ空っぽのHTMLが返ってくる
  2. JavaScirpt(React含む)が読み込まれる
  3. DOMが描画される

ワイ「こんな感じで、画面が表示されるまでに時間がかかってしまって」
ワイ「ユーザーさんをお待たせしてしまうやろ?」

娘「そっか」
娘「サーバからHTMLをもらった時点で」
娘「HTML要素たちが書かれていれば」
娘「その方が速いもんね」

ワイ「せやで」

で、ハイドレーションとは

娘「で、ハイドレーションはいつ出てくるの?」

ワイ「せやったな」
ワイ「さっきの、サーバから返ってきたHTMLには」
ワイ「色んなHTML要素が書かれてはいるんやけど」
ワイ「まだJavaScriptのイベントリスナ等は登録されてないんや」
ワイ「せやから、クライアント側のJavaScriptによって」
ワイ「イベントリスナが登録されたり、インタラクティブな動作が追加されるんや」

娘「うんうん」

ワイ「それがハイドレーションや」

娘「へぇ〜」
娘「サーバから来たHTMLに」
娘「Reactのインタラクティブな機能が注ぎ込まれる」
娘「それがハイドレーションなんだね」

ワイ「せやな」
ワイ「水和とか水分補給とか、そういう意味の単語らしいで」

娘「水を加えるってこと?」

ワイ「そんな感じやな」
ワイ「サーバから受け取った初期HTMLは、インタラクティブな機能を持たない乾いたHTMLって感じで」
ワイ「そこに、クライアント側で水分を加えてやるイメージやな」

娘「へぇ〜」
娘「でも、何でそんなことするの?」

ワイ「サーバからHTMLが来た時点で、もう要素たちは書かれてるわけやから」
ワイ「それをわざわざクライアント側のJavaScirptで丸ごと置き換えたりせずに」
ワイ「サーバから受け取ったHTMLを再利用して、そこにイベントリスナなどを登録してやる感じや」

娘「そっか」
娘「そう考えると効率的だね」
娘「クライアントサイドで、インタラクティブなDOMをもう一度作り直すんじゃなくて」
娘「サーバから来たHTMLを再利用するんだね」

ワイ「せやで」

じゃあ、ハイドレーションエラーとは

娘「ハイドレーションは分かったけど」
娘「私の作ったサイコロアプリは、何でハイドレーションエラーが出ちゃったんだろ?」

ワイ「うーん」
ワイ「乱数があるから、クライアントサイドのJSくんが困ってしまうんやろなぁ」

クライアントサイドJSくんのお気持ち

JSくん「よーし、お仕事するでぇ」
JSくん「ハイドレーションしていくでぇ」
JSくん「ほな、サイコロページのコンポーネントを見てみよか」
JSくん「なるほど〜、こういったコンポーネントがあるってことは」
JSくん「たぶん、サーバからは───」

クライアントサイドJSくんの予想するHTML
  <div id="__next">
    <h1>サイコロの目</h1>
    <p>5</p>
    <button>サイコロをふる</button>
  </div>

JS「↑これと一致するHTMLが返って来てるはずや!」
JS「ワイはその中にあるボタンに、イベントリスナなんかを加えていくでぇ」
JS「どれどれ、実際にサーバから返って来たHTMLを見てみよか2
JS「・・・ありゃ!?」

実際にサーバくんが返した初期HTML
  <div id="__next">
    <h1>サイコロの目</h1>
    <p>6</p>
    <p>やった!6だね!</p>
    <button>サイコロをふる</button>
  </div>

JS「思ってたHTMLと違うやんけ・・・!」
JS「ほな、どのボタンにイベントリスナを加えればええんや・・・?」
JS「これ、再利用できひんわ!エラー投げて開発者さんに教えてあげんと!」


ワイ「↑こんなイメージやな」
ワイ「ランダムな数値を扱ったせいで」
ワイ「サーバから来る初期HTMLと、クライアントサイドJSくんが予想してたHTMLにズレが生じてしまって」
ワイ「ハイドレーション中にエラーが出てもうたんやな」

娘「そっかぁ」
娘「思ってたのと違うHTMLが来たら、使えないもんね」

ワイ「せやでぇ」
ワイ「サーバから貰った初期HTMLを再利用できへんとなると」
ワイ「パフォーマンスも低下してしまうから、よろしくないでぇ」

じゃあ、どうすればいい?

娘「じゃあ、ランダムな値を使いたい場合はどうすればいいの?」

ワイ「クライアント側の予想するHTMLと、サーバから来る初期HTMLを、一致させなアカンから・・・」
ワイ「サイコロの値は、初期値でnullを設定するようにして」
ワイ「<p>5</p>とか<p>6</p>とか、そういう乱数は表示されないようにしてみよか」

  // ランダム値ではなく null を初期値とする。
  const [saikoroNumber, setSaikoroNumber] = useState<number | null>(null)

  return(
    <>
      <h1>サイコロの目</h1>
      {/* saikoroNumber が null の場合は表示しない */}
      {saikoroNumber !== null && <p>{ saikoroNumber }</p>}
      {/* 6の場合はメッセージを表示 */}
      {saikoroNumber === 6 && <p>やった!6だね!</p>}
      <button onClick={roll}>サイコロをふる</button>
    </>
  )

ワイ「こうしておけば、初期HTMLが作られるときには、サイコロの目はnullになるから」
ワイ「サーバから返ってくるHTMLは───」

サーバくんが返す初期HTML
<div id="__next">
  <h1>サイコロの目</h1>
  <button>サイコロをふる</button>
</div>

ワイ「↑こうなるはずや」

娘「なるほどね」
娘「サーバから返ってくる初期HTMLに、ランダム値が含まれないようにするんだね」

ワイ「そういうことや」
ワイ「そうすると、クライアントサイドJSくんも───」

JSくん「サイコロの初期値はnullやから」
JSくん「↓こんなHTMLが返って来てるはずや!」

クライアントサイドJSくんの予想するHTML
<div id="__next">
  <h1>サイコロの目</h1>
  <button>サイコロをふる</button>
</div>

JSくん「ほな、サーバから来たHTMLを見てみるでぇ」

実際にサーバくんが返した初期HTML
<div id="__next">
  <h1>サイコロの目</h1>
  <button>サイコロをふる</button>
</div>

JSくん「よし、思ってた通りのHTMLが来てるで!」
JSくん「ほな、ここにJSのインタラクティブな機能を注いでいくでぇ!」

ワイ「↑こう、ちゃんとハイドレーションができるわけやな」

娘「へぇ〜」
娘「でも、その直後くらいに、nullからランダムな数値に変えないといけないよね?」
娘「そうしないと、サイコロの目の数が表示されないよ?」

ワイ「せやな」

娘「うまいことそのタイミングで乱数を生成するには、どうすればいいの?」

ワイ「そんなときは、アレを使うんや」

娘「アレ・・・?」

ワイ「ビルド時やサーバ側では実行されずに」
ワイ「クライアントサイドに来てから実行される───」
ワイ「そんな hooks があったよな」

娘「え・・・useEffect()を使うってこと?」

ワイ「せや」

const SamplePage = () => {
  const [saikoroNumber, setSaikoroNumber] = useState<number | null>(null)

+ useEffect(() => {
+   setSaikoroNumber(saikoro())
+ },[])

/* 以下略 */

ワイ「↑こうやな」
ワイ「useEffect()に渡した処理は、クライアント側でしか実行されへんし」
ワイ「ハイドレーションの後に実行されるんや」
ワイ「せやから、これでハイドレーションエラーは起こらんくなるで」

娘「なるほど〜」
娘「ありがとう、パパ!」

まとめ

  • コンポーネントは、クライアント側だけでなくビルド時やサーバ側でも実行される
  • ハイドレーションとは
    • サーバから受け取った「乾いたHTML」に、クライアントサイドのインタラクティブな機能を注ぎ込むこと
      • イベントリスナを登録したり、ステート管理やその他の動的な機能を追加する
  • ハイドレーションエラーとは
    • 「サーバから受け取った初期HTML」と「クライアントサイドJSが予期するHTML」が一致しない場合に起こるエラー
    • 予想と違うHTMLが来ると、どの要素に機能を注ぎ込めばいいか特定できず、再利用できない
  • ハイドレーションエラーを起こさないためには
    • 「サーバから受け取った初期HTML」と「クライアントサイドJSが予期するHTML」が一致するようにコードを書く
    • 【例】サーバから来る初期HTMLにランダム値が含まれないようにする
  • クライアント側に来てから実行したいものはuseEffect()の中に書く
    • useEffect()はハイドレーションの後に実行される

娘「↑こういうことだね!」

ワイ「せやな!」

娘「windowlocalStorageなんかも、サーバ側やビルド時には存在しないから」
娘「サーバサイドとクライアントサイドの差異を生みそうだね!」

ワイ「せやな」
ワイ「そういうブラウザ固有のAPIを使用する処理なんかも」
ワイ「useEffect内に移した方がええな」

娘「なるほどね〜」
娘「じゃあ、続きを実装してみる!」

ワイ「おう、頑張ってや!」

〜おしまい〜

参考文献

  1. SWRやReact Queryで取得する外部APIのデータなど、初期では書かれていない部分も勿論あります

  2. 実際には「HTMLを見比べる」訳ではなく「サーバから来たHTMLをもとに作られた実DOM」と「クライアント側でReactが生成した仮想DOM」の状態が一致することを確認します

145
54
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
145
54