Warning: Prop `className` did not match.Server: xxx の対処

2020/09/27に公開

この Warning で本ページにランディングした方こんにちは。
書いてある通りですが、className がサーバーのレンダリングとマッチしていない場合への対処方法を記しました。また環境は next.js 9.5です。

前提

まず、今回なぜ Server(SSR) と class名が異なってしまう原因は当然、サーバーとブラウザの実行結果の差異にあります。
今回は geist というReact UI library の use-media-query の実行結果の差異が原因です(例はなんでもいいのですが...)

ドキュメントの例にある通り以下のコードを実行したとして サーバーでは必ず false, クライアントによっては 画面の横サイズが 1280px(デフォルト値) 以上の 場合 true, それ以外 false となります

() => {
  const upMD = useMediaQuery('md', { match: 'up' })
  return (
    <>
      Current screen width <b>{upMD ? '>=' : '<='}</b> <Code>md</Code>.
    </>
  )
}

しかし、これでは困ります。
結果が同じ場合には問題ありませんが、upMD の値が true の場合は表題のワーニングが発生します。

useMediaQuery のソースを読むと以下のようになっています。
ここを読んでもとりあえず false を返しているのがわかります。
(bool を返す関数なのに、とりあえずなものを戻り値にしてもいいのかな〜、普通にサーバーで呼び出 び出した際はエラーか undefind を返した方がいいのではと思いつつ...)

ならば クライアントのみで呼び出すようにしようと考えました。


...
  /**
   * Do nothing in the server-side rendering.
   * If server match query fucntion is simulated, return user-defined value first.
   */
  const [state, setState] = useState<boolean>(() => {
    if (supportMedia) return matchQuery(query).matches
    if (ssrMatchMedia && typeof ssrMatchMedia === 'function') {
      return ssrMatchMedia(query).matches
    }
    return false
  })
...

対処

クライアントのみで実行するようにします。
具体的には isClient の Costom Hooks を呼び出して実行分岐ロジックを書くだけです。

useClient は以下

import { useState, useEffect } from 'react'

export const useClient = (): boolean => {
  const [isClient, setIsClient] = useState(false)

  useEffect(() => {
    if (typeof window !== 'undefined') setIsClient(true)
  }, [])

  return isClient
}

() => {
  const upMD = useMediaQuery('md', { match: 'up' })
  const isClient = useClient()
  return (
    <>
     {isClient && Current screen width <b>{upMD ? '>=' : '<='}</b> <Code>md</Code>.}
    </>
  )
}

まとめ

非常に簡単な Costom Hooks ですが、SSR の際に実行したくない(だけ実行したい)場合に便利です。
もっと便利なのを思いついたら追記します。

Discussion