何をするか?
React (Next.js) の useState
フックは、Web ページの状態を保持するものですが、ページのリロードや、ブラウザの再起動を行うと、その状態はリセットされてしまいます。
一方、Web ブラウザに搭載されている localStorage や sessionStorage を使用すると、キー&バリュー(両方とも文字列のみ)の形でデータを保存することができます。
ここでは、これらを一緒に使うことで、useState
で管理している状態をローカルストレージに保存・復帰できるようにしてみます。
使い方のイメージ
例えば、Web サイト上でダークモードの On/Off を行うスイッチがあるとして、その状態をローカルストレージに保存できるようにしたいとします。

ダークモードの状態は useDarkMode
のようなカスタムフックを作成して、次のように扱えると便利です。
import { NextPage } from 'next'
import { useDarkMode } from '../hooks/useDarkMode'
const SamplePage: NextPage = () => {
// 一見 useState と同様だが localStorage と連動している
const [isDark, setDark] = useDarkMode(false)
return (
<div
style={{
width: '100vw',
height: '100vh',
color: isDark ? 'white' : 'black',
background: isDark ? 'black' : 'white',
}}
>
<label>
<input
type="checkbox"
checked={isDark}
onChange={(e) => setDark(e.target.checked)}
/>
{isDark ? 'DARKモードです' : 'LIGHTモードです'}
</label>
</div>
)
}
export default SamplePage
useDarkMode の実装
useState
による状態をローカルストレージと連動させるには、状態の初期化時に localStorage.getItem
で値をロード、状態の変更時に localStorage.setItem
で値をセーブ、とすればよさそうです。
ただし、localStorage
オブジェクトは、クライアントサイド JS でしか参照できないため、Next.js などのサーバーサイドレンダリング時に値を参照しようとするとエラーになってしまいます。
localStorage
の参照タイミングをうまく制御しながら、useState
フックと連携させなければいけません。
useEffect を使う方法
useEffect
(副作用フック)で設定したコールバック関数は、クライアントサイドでのレンダリング時にしか呼び出されないことが保証されているので、次のように localStorage.getItem
を呼び出してやれば、状態の初期化はうまくいきます。
import { useCallback, useEffect, useState } from 'react'
/**
* ダークモード設定を保存するローカルストレージのキー名。
* ダークモードなら `true` という文字列を格納する。
*/
const STORAGE_KEY_DARK_MODE = 'myapp.example.com/darkMode'
/**
* ダークモード設定状態を扱うためのフック。
*/
export function useDarkMode(
defaultValue: boolean
): [isDark: boolean, setDark: (dark: boolean) => void] {
const [isDarkInternal, setDarkInternal] = useState(defaultValue)
// クライアントでの初期レンダリング直後にローカルストレージの設定を反映
useEffect(() => {
const dark = localStorage.getItem(STORAGE_KEY_DARK_MODE) === 'true'
if (dark !== defaultValue) {
setDarkInternal(dark)
}
}, [setDarkInternal])
// 外部からのセッター呼び出し時にローカルストレージに値を保存する
const setDark = useCallback(
(isDark: boolean) => {
localStorage.setItem(STORAGE_KEY_DARK_MODE, isDark ? 'true' : 'false')
setDarkInternal(isDark)
},
[setDarkInternal]
)
return [isDarkInternal, setDark]
}
初期表示時のフラッシュ問題
useEffect
は初回レンダリング後に呼び出されるので、Web ブラウザをリロードしたときに、デフォルトの状態(上記の例の場合は isDark = false
)で描画されてしまうことを防ぐことができません。
ユーザーがダークモードに設定していたとしても、ブラウザのリロードを行うと、瞬間的にライトモードの(SSR 生成された)ページが見えてしまいます。
この問題の解決方法はいろいろ考えられますが、いずれも若干トリッキーな実装が必要になるみたいです。 参考になりそうなサイトから、ポイントだけ抜粋しておきます。
Adding dark mode with Next.js, styled-components, and useDarkMode
SSR 時には全体を
visibility: 'hidden'
で描画しておき、初回レンダリング後(useEffect
コールバック後)に通常表示に切り替える方法。実はデフォルトモードで描画してるんだけど、見えないようにしてるということ。const [mounted, setMounted] = React.useState(false) React.useEffect(() => { setMounted(true) }, []) // prevents ssr flash for mismatched dark mode const body = <ThemeProvider theme={theme}>{children}</ThemeProvider> if (!mounted) return <div style={{ visibility: 'hidden' }}>{body}</div> return body
donavon/use-dark-mode: A custom React Hook to help you implement a “dark mode” component.
_document.tsx
で特殊な noflash.jsをロードするようにしておき、その中で次のように全体に反映される CSS クラスを設定する方法。この JS ファイルはクライアントサイドでの初期レンダリング時に必ず実行されるので、確実にlocalStorage
の値を CSS クラスに反映できる。(function() { // ... document.body.classList.add(darkMode ? classNameDark : classNameLight) // ... })()
いやぁ。なかなか大変ですね。。。
とはいえ、現在のテーマ設定値に関しては、少なくとも React のコンテキスト として保持するようにしておけば、ページ遷移時に画面がフラッシュするようなことは防げます。
というわけで、ここでサンプルコードとして使ったuseDarkMode
の例はあまりよくなかったかもです。。。
関連記事
- Next.js から AWS DynamoDB にアクセスする
- Next.js で開発環境で実行しているときに Web サイト上に dev 表示する (TargetEnvIndicator)
- Next.js で src からの相対パスで import できるようにする (tsconfig.json)
- Next.js のページコンポーネントが Client と Server どちらで実行されているか調べる (isServer, isClient, NoSsr)
- Next.js アプリでのリンク方法まとめ(mui/Material-UI との連携なども) (next/link, next/router)
- Next.js の public 以下のファイルのパスを正しく扱う
- ESLint (4) ESLint の設定方法まとめ (for Next.js 11)