【react-hydration-error】Text content does not match server-rendered HTMLを解決するときの話

# はじめに
業務でNext.jsを使った開発を3ヶ月ほど行っています。
その際、Text content does not match server-rendered HTMLという開発時のエラーに遭遇しました。
解決自体は難しくなかったのですが、最終的にはググって見つけた方法やChatGPTに答えてもらった方法とは異なる解決法を用いました。
今回は一般的な解決法と今回使用した解決法について紹介し、簡単ではありますが、エラー文の検索だけでは最良の解決法にたどり着けない具体例を示そうと思います。
Reactの話ですが、触ったことがなくても雰囲気で読めると思います。

# エラーが発生したコード
簡略化していますが、以下のコードで上記のエラーが発生しました。このコンポーネントはSSR(サーバーサイドレンダリング、サーバーに問い合わせてレンダリングする方法)で描画しています。
commentが30文字以内のときはそのまま表示して、30文字以上あるときは30文字まで表示して、末尾に「...」を加えるというコードです。
```jsx
function MyComponent({comment}) {
  const displayedComment = comment.length > 30 
                            ? comment.slice(0, 30) + '...'
                            : comment
  return <div>{displayedComment}</div>
}
```
ここで、修正方法がわかってしまった方はそっとブラウザバックしてください笑。
意外とわからないと思います。
(ちなみにcommentはちゃんとstring型です。nullとかundefinedのときlengthがとれないという話ではないです。今回は簡単のためJSで書いています)

# 発生したエラーの内容と一般的な解決法
[Text content does not match server-rendered HTML](https://nextjs.org/docs/messages/react-hydration-error)はサーバー側とブラウザ側でのレンダリングにずれが発生したときに出るエラーです。
エラー文でググって見つける（もしくはChatGPTに教えてもらう）ことのできる、公式で提示されている修正方法を見ていきましょう。

1. window等のブラウザ側でしか動かないライブラリ、アプリケーションコードの使用を避ける
    windowはサーバー側でのみundefinedなので、以下のコードではサーバー側とブラウザ側でズレが生じます。
    ```jsx
    function MyComponent() {
      const color = typeof window !== 'undefined' ? 'red' : 'blue'
      return <h1 className={`title ${color}`}>Hello World!</h1>
    }
    ```
    これを解決する方法としてレンダリング後に呼ばれるuseEffect()の使用があげられています。
    ```jsx
    import { useEffect, useState } from 'react'
    function MyComponent() {
      const [color, setColor] = useState('blue')
      useEffect(() => setColor('red'), [])
      return <h1 className={`title ${color}`}>Hello World!</h1>
    }
    ```
    また、この例とは少し状況が異なり、そもそもサーバーサイド側でレンダリングしたくない場合は[Dynamic Import](https://nextjs.org/docs/advanced-features/dynamic-import#with-no-ssr)の機能を用いることもできます。
    ```jsx
    import dynamic from 'next/dynamic'
    
    const DynamicHeader = dynamic(() => import('../components/header'), {
      ssr: false,
    })
    ```

2. HTMLのコンテンツモデルに則った構造に修正する
    HTMLのタグの親子関係の組み合わせには制限があり、そのルールのことをコンテンツモデルといいます。
    詳しくはググったりしてほしいのですが、一例としてpタグの子要素としてdivタグを使用する例が挙げられています。
    ```jsx
    export const IncorrectComponent = () => {
      return (
        <p>
          <div>
            This is not correct and should never be done because the p tag has been
            abused
          </div>
          <Image src="/vercel.svg" alt="" width="30" height="30" />
        </p>
      )
    }
    ```
    これはpタグをdivタグに変えることで解決することができます。
    ```jsx
    export const CorrectComponent = () => {
      return (
        <div>
          <div>
            This is correct and should work because a div is really good for this
            task.
          </div>
          <Image src="/vercel.svg" alt="" width="30" height="30" />
        </div>
      )
    }
    ```

3. エラーを無視する属性を追加する
    上の２つの方法に加えて、強制的にエラーを発生させないようにするJSXの属性も[Reactの公式ドキュメント](https://react.dev/reference/react-dom/hydrate#suppressing-unavoidable-hydration-mismatch-errors)で説明されています。
    ```jsx
    export default function App() {
      return (
        <h1 suppressHydrationWarning={true}>
          Current Date: {new Date().toLocaleDateString()}
        </h1>
      );
    }
    ```
    この例ではタイムスタンプを用いており、どうしてもサーバー側のレンダリングとブラウザ側のレンダリングでズレが生じるので
    ```
    suppressHydrationWarning={true}
    ```
    というコードを追加することでエラーの発生を防いでいます。

# 今回採用した解決法

今回はコメントの表示できる文字数に制限がかかっており、ちょうど末尾に絵文字が表示される際にこのエラーが発生しました。
特に絵文字や一部の漢字はサロゲートペアという2つの特殊なUnicodeコードポイントの組み合わせで表現されます。
その2文字がsliceによって1文字だけ切り取られてしまった場合、今回のエラーが発生していました。
今回は上にあげた解決法でエラー自体はなくなるのですが、文字化けはなくなりません。
なので、字数判定のロジックをサロゲートペアを考慮したものに変えることが最良の解決法になります。
ここでは配列に変換して取り扱う方法を採用しました。

```jsx
function MyComponent({comment}) {
  const displayedComment = [...comment].length > 30 
                            ? [...comment].slice(0, 30).join('') + '...'
                            : comment
  return <div>{displayedComment}</div>
}
```

# まとめ
簡単な例ですが、「エラーの解決方法は必ずしもそのエラー文で検索した方法が最良とは限らない」ということがわかって勉強になりました。
もちろんエラー文で検索した内容を試すのは大事ですが、どうしてエラーが発生するのか理解しようとするのも大事ですね。
皆様も良きエラー解決ライフを！！