😮

Next.js13でEmotionを使うのにuseEffectが必要だと思ったら全然そんなことなかった

2023/03/21に公開約5,400字

それはNext.jsのハンズオンにて起こった

Warning: Prop `className` did not match.Server・・・・・

こういうエラーが発生し、ビルドが通らない事態となってしまいました。
発生箇所はとある単一ファイルコンポーネントで、親のページコンポーネントからPropsを受け取って展開するシンプルなものです。

今回の場合はCSSの記述にEmotionを採用したのですが、どうもそれに由来しているエラーのようでした。

そこでググってみたところ下記の記事にたどり着き、まずはそのまま真似させていただいたわけです。

https://zenn.dev/takewell/articles/5ee9530eedbeb82e4de7

import { useState, useEffect } from 'react'

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

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

  return isClient
}
import { useClient } from "../../hooks/useClient";
import { css } from "@emotion/react";

const someComponent = () => {
  const isClient = useClient()

  return (
    <>
      {
        isClient ? 
          <div
            css={css`~~~~`}
          >
            ~~~~~~~~
          </div>:
          <></>
      }
    </>
  )
}

この修正によってひとまず動くようになりました。

しかし私はこのときReactを触り始めてまだ一週間も経っておらず、このカスタムフックの意味は全然わからないのです。
まずはここで使っているuseStateuseEffectというものについて勉強してみました。

stateとは

先にstateについて理解する必要がありそうです。

Reactにおけるstateは、コンポーネントが内部で持つ状態のことを指すようです。
これはおそらく、Vue.jsの2系でいうところのdataオブジェクトに相当するものでしょう。

Vue.jsでは、dataオブジェクトが更新されると自動的に参照先のViewも更新されます。
少なくとも関数型コンポーネントにおけるstateの説明を見ていると、Reactでも事情は同じように見えます。

フックとは

Reactにおけるフックとは、そのようなstateを定義・更新するための機構であると考えてよさそうです。
ちょうど読みたかったような記事があったのでさっそく参照していますが、最も基本的なフックであるuseStateの説明を読んでいるとそのような印象が強いです。

https://qiita.com/seira/items/f063e262b1d57d7e78b4

Vue.jsでいうdataメソッドみたいなものですかね。あれはdataオブジェクトを定義するためのメソッドですが、useStateはちょうどそれに対応するメソッドのように思えます。

import { useState, useEffect } from 'react'

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

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

  return isClient
}

今回真似したカスタムフックでは、isClientというプロパティ、並びにそれのセッター関数を宣言し、初期値としてfalseを与えるという処理を行っていました。
Vue.js(かつOptions API)だとこんな感じなんですかね。

export default {
  data: () => ({
    isClient: false // 初期値
  }),
  methods: {
    setIsClient(bool) {
      this.isClient = bool
    }
  }
}

ではuseEffectとはなんでしょう。

ライフサイクルメソッドに相当するもの

React のドキュメントにはこんなヒントが書いてありました。

React のライフサイクルに馴染みがある場合は、useEffect フックを componentDidMount と componentDidUpdate と componentWillUnmount がまとまったものだと考えることができます。

なんとなくVue.jsでも見覚えがあります。要するに、ページやコンポーネントのライフサイクルのどこで何をするか指定できるものということです。
今回のカスタムフックではどのようなタイミングでプロパティを更新しようとしたのでしょうか。

答えは画面のレンダリング後であるようです。
Vue.jsならこういうことでしょうか。

export default {
  data: () => ({
    isClient: false // 初期値
  }),
  mounted() {
    if (typeof window !== 'undefined') setIsClient(true) // 読み込み後に実行
  },
  methods: {
    setIsClient(bool) {
      this.isClient = bool
    }
  }
}

componentDidMountcomponentDidUpdatecomponentWillUnmountは、、VueであればmountedupdatedbeforeUnmountがそれぞれ対応すると思われます。関数名のニュアンスからして多分そう。
このうちmountedは初回レンダリング、updatedはデータの更新時に実行されるので、読み込み範囲こそ違いますが、いずれにしても画面のレンダリングに連動して実行される処理であるということは共通しています。
であるならば、たしかにメソッドはuseEffectひとつでもよさそうです。

useEffectは第二引数に、第一引数の関数式を実行するトリガーとなる値を列挙した配列を受け取るようですが、今回のカスタムフックでは第二引数の配列は空です。
つまりこのuseEffectは、データの更新には連動せず、初回読み込み時(ページそのもののレンダリング時)にのみ実行されるもののように見えます。

カスタムフックとは

簡単に言えば、複数のHookをパッケージングできる機能と説明できるかもしれません。
もう少し具体的な目的としては、各コンポーネントで共通する「状態に合わせて実行されるロジック」を共通化することにあるようです。

Vue.jsでは、そうしたことは Vuex の actions でやるとか、子から$emitしたイベントを親で検知するとか、ミドルウェアでページ遷移時に状態を見るとか、そういう方法でコンポーネント間の状態の共有を行っていました。
Reactのカスタムフックは、そうした状態変更と状態を更新した後の処理という一連の流れにいい感じの命名ができる、という理解でも良さが実感できそうです。

今回のエラーはどういうエラーだったのか

カスタムフックuseClientの処理はそのようなものでした。

ページにアクセスがあった場合、最初にwindowundefinedであるかどうかを確認する。
undefinedでなければ実行環境がサーバーではないと判断し、真偽値isClienttrueに更新する。
そのようにセットされたisClientをコンポーネントから参照し、trueであった場合のみDOMの描画を行う。

エラーメッセージ曰く、Emotionが生成するclassName属性の値がサーバーとクライアントで一致していないという旨を示していました。
よくわかんないけど、なんか関係ありそうという雰囲気は感じます。

メッセージで検索すると、EmotionのGithubリポジトリにて、同じエラーに関するissueが上がっているのが見つかります。
ここでレスを付けているEmotion開発メンバーのAndarist氏は、文中でこのような助言を提示しています。

node_modules に複数の babel-plugin-emotion が入っていないか確認してみてください

なんとなくピンと来ました。実はこれ以外にもう一つ、ビルドが通らなくなるエラーに対処していたのですが、そっちのエラーはEmotion導入時に追加したbabel-plugin-emotionと、Next.jsのコンパイラ(SWC)が競合していたのが原因のようでした。
Emotionの導入について私が参照した記事の内容がどうやら古くなっていたようで、現在のNext.jsはデフォルトでEmotionに対応しており、わざわざコンパイルの設定を追加しなくてもよかったのです。

babel-plugin-emotionを削除することでそのエラーは解消され、Emotionもきちんと使えていることを確認できました。

どう解決されたのか

今回のエラー/className/ did not match.Server・・・・・に関しても、そういうなんかがあれしているのでは?と勘づいた私は、件のコンポーネントからカスタムフックの呼び出しと、戻り値による条件分岐を削除してみました。

// const isClient = useClient()

  return (
    <>
      {/* {
        isClient ?  */}
          <div
            css={css`~~~~`}
          >
            ~~~~ ちゃんと表示された ~~~~
          </div>
          {/* <></>
      } */}
    </>
  )

問題なくビルドが通りました。つまり不要なBabelプラグインを削除した今となっては、このカスタムフックは不要であったのです。

まとめ

かなり情報の鮮度にムラがあって、ある程度Reactの歴史や機能追加の経緯に理解がないと混乱してしまいそうだと感じました。

  • かつてはクラス式でコンポーネントを書いていたが、現在は関数で書く
  • かつてはJSXのコンパイルやJSのトランスパイルなどにはBabelが使われていたが、現在はRust製のSWCが速くて人気

参入したばかりだとこのへんが疎いです。

Vue.jsの時ほど「なんとなく」ではやれなさそうというか、実践ばかりではなくしっかりめに座学を積み重ねないといけなさそうだな、という印象を受けました。

参考リンク

https://zenn.dev/takewell/articles/5ee9530eedbeb82e4de7
https://github.com/vercel/next.js/issues/7322
https://ja.reactjs.org/docs/hooks-effect.html
https://ja.reactjs.org/docs/hooks-overview.html
https://ja.reactjs.org/docs/hooks-custom.html
https://qiita.com/seira/items/f063e262b1d57d7e78b4
https://qiita.com/seira/items/e62890f11e91f6b9653f

GitHubで編集を提案

Discussion

ログインするとコメントできます