🫠

useMediaQuery Hydration Error【備忘録】

2023/02/09に公開

概要

react-responsiveというライブラリをNext.jsで使おうとすると、
React Hydration Errorが出てきて、永遠にバグる、、、というエラーに遭遇しました。
なかなか解決できない中、GithubのIssueで素晴らしい海外ニキが
仕様まで読み込んで解決していたので、備忘録として記事にさせていただきました🙇

useMediaQueryの仕様

Chakura UIのGithubでの注意書き

この API は、ユーザーのブラウザによる window.matchMedia のサポートに依存しており、サポートされていない場合や存在しない場合は常に false を返すことに留意してください (サーバーサイドレンダリングの場合など)。

といった記述があったそうで、コードは以下になります。

function useMediaQuery() {
  if (typeof window === "undefined") {
    throw new Error("useMediaQuery() can not be used outside of the browser");
  }}

これを見てみると、HookがSSRの間に使用できることを示していて、サーバーで使わない場合、
バグを発生させるのではなく、アプリをクラッシュさせる仕様です。

え、、、???
そもそもuseMediaQueryがSSRでしか動かない実装??
わんちゃんreact-responsiveもそういう感じっぽいですが、一応確認してみます。

実際にGithubを覗いてみる

ということで、GithubのレポジトリへGo!

useMediaQuery.ts
const useMediaQuery = (settings: MediaQuerySettings, device?: MediaQueryMatchers, onChange?: (_: boolean) => void) => {
  const deviceSettings = useDevice(device)
  const query = useQuery(settings)
  if (!query) throw new Error('Invalid or missing MediaQuery!')

// 長いので省略

  return matches
}

似たような実装をされてる雰囲気、、、😇

ということは、ChakuraUIのuseMediaQuery同様に
海外ニキの手法を取ることが正解っぽいですね、、、🤔
(具体的な仕様、動作する実装方法がわかる方いたら教えていただきたいです🙏)

対処法

海外ニキが出した答えは、カスタムフックを作ることでした!
先ほどの実装内にあるバグを削除したものを
カスタムフックとして作成することで動作させているようです🙌

hooks/useMediaQuery.ts
import { useEffect, useState } from 'react'

const useMediaQuery = (mediaQueryString: string) => {
  const [matches, setMatches] = useState<boolean>(false)

  useEffect(() => {
    const mediaQueryList = window.matchMedia(mediaQueryString)
    const listener = () => setMatches(!!mediaQueryList.matches)
    listener()
    mediaQueryList.addListener(listener)
    return () => mediaQueryList.removeListener(listener)
  }, [mediaQueryString])

  return matches
}

export default useMediaQuery

このフックを使うことで、Hydration Errorが消えて動作しました!

訂正

addlistener, removelistnerは非推奨ですが、上記の方法では使用していました。
こちらをaddEventListener, removeEventListenerで書き換えると

useMediaQuery.ts
import { useEffect, useState } from 'react'

const useMediaQuery = (mediaQueryString: string) => {
  const [matches, setMatches] = useState<boolean>(false)

  useEffect(() => {
    const mediaQueryList = window.matchMedia(mediaQueryString)
    const listener = () => setMatches(!!mediaQueryList.matches)
    listener()
    mediaQueryList.addEventListener('change', listener)
    return () => mediaQueryList.removeEventListener('change', listener)
  }, [mediaQueryString])

  return matches
}

export default useMediaQuery

上記のようになります🙇
MDNを見ても非推奨になっている(=今後動作しなくなる可能性がある)ので、
IDEでdeprecatedが見えた時には使わないようにしましょう!

参考

https://developer.mozilla.org/en-US/docs/Web/API/MediaQueryList/addListener

終わりに

今回は、Githubの中まで少し覗く結果になりました。
たまーに仕様を見て、エラーの原因を探ったり、
今回のように既存の海外ニキの解決法を見る→覗いて原因を探ったり
といったことがあります。
みなさんもどうしてもあってるはずなのにつまずいた時は
Deeplの拡張機能で翻訳してもいいので、Githubを覗いてみてください🔥
それでは👋

参考文献

https://github.com/chakra-ui/chakra-ui/issues/3580

https://github.com/yocontra/react-responsive

https://developer.mozilla.org/en-US/docs/Web/API/MediaQueryList/addListener

Discussion