Next.js13でEmotionを使うのにuseEffectが必要だと思ったら全然そんなことなかった
それはNext.jsのハンズオンにて起こった
Warning: Prop `className` did not match.Server・・・・・
こういうエラーが発生し、ビルドが通らない事態となってしまいました。
発生箇所はとある単一ファイルコンポーネントで、親のページコンポーネントからPropsを受け取って展開するシンプルなものです。
今回の場合はCSSの記述にEmotionを採用したのですが、どうもそれに由来しているエラーのようでした。
そこでググってみたところ下記の記事にたどり着き、まずはそのまま真似させていただいたわけです。
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を触り始めてまだ一週間も経っておらず、このカスタムフックの意味は全然わからないのです。
まずはここで使っているuseState
とuseEffect
というものについて勉強してみました。
stateとは
先にstateについて理解する必要がありそうです。
Reactにおけるstateは、コンポーネントが内部で持つ状態のことを指すようです。
これはおそらく、Vue.jsの2系でいうところのdataオブジェクトに相当するものでしょう。
Vue.jsでは、dataオブジェクトが更新されると自動的に参照先のViewも更新されます。
少なくとも関数型コンポーネントにおけるstateの説明を見ていると、Reactでも事情は同じように見えます。
フックとは
Reactにおけるフックとは、そのようなstateを定義・更新するための機構であると考えてよさそうです。
ちょうど読みたかったような記事があったのでさっそく参照していますが、最も基本的なフックであるuseState
の説明を読んでいるとそのような印象が強いです。
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
}
}
}
componentDidMount
・componentDidUpdate
・componentWillUnmount
は、、Vueであればmounted
・updated
・beforeUnmount
がそれぞれ対応すると思われます。関数名のニュアンスからして多分そう。
このうちmounted
は初回レンダリング、updated
はデータの更新時に実行されるので、読み込み範囲こそ違いますが、いずれにしても画面のレンダリングに連動して実行される処理であるということは共通しています。
であるならば、たしかにメソッドはuseEffect
ひとつでもよさそうです。
useEffect
は第二引数に、第一引数の関数式を実行するトリガーとなる値を列挙した配列を受け取るようですが、今回のカスタムフックでは第二引数の配列は空です。
つまりこのuseEffect
は、データの更新には連動せず、初回読み込み時(ページそのもののレンダリング時)にのみ実行されるもののように見えます。
カスタムフックとは
簡単に言えば、複数のHookをパッケージングできる機能と説明できるかもしれません。
もう少し具体的な目的としては、各コンポーネントで共通する「状態に合わせて実行されるロジック」を共通化することにあるようです。
Vue.jsでは、そうしたことは Vuex の actions でやるとか、子から$emit
したイベントを親で検知するとか、ミドルウェアでページ遷移時に状態を見るとか、そういう方法でコンポーネント間の状態の共有を行っていました。
Reactのカスタムフックは、そうした状態変更と状態を更新した後の処理という一連の流れにいい感じの命名ができる、という理解でも良さが実感できそうです。
今回のエラーはどういうエラーだったのか
カスタムフックuseClient
の処理はそのようなものでした。
ページにアクセスがあった場合、最初に
window
がundefined
であるかどうかを確認する。
undefined
でなければ実行環境がサーバーではないと判断し、真偽値isClient
をtrue
に更新する。
そのようにセットされた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の時ほど「なんとなく」ではやれなさそうというか、実践ばかりではなくしっかりめに座学を積み重ねないといけなさそうだな、という印象を受けました。
参考リンク
Discussion