Reactでダークモードを再開した with Gatsby

2024/03/31 9 minute read

Gatsbyプラグインなし + localStorage + React Hook

もくじ

感想

このサイトはContentful製 : starter-gatsby-blogからはじまっているので、 Blogページだけ趣が違うというか、ほぼ原型。
もともと白ベースで作られていたのを見慣れていたから、前回プラグインで実装したときは、自分はほとんど使うことはなかった。

ただし閲覧者は別。ダークモード対応するしないは、配慮の問題と考えている。


かなり前に、Webクリエーターボックス : ダークモードに対応していないWebサイトを無理やりダークモードにする拡張を読んだとき、先に書かれたWebサイトをダークモードに対応させようという記事ではよく理解していなかった「スターバースト現象」というものに (え?!) となり、以来ダークモードを実装するなら極力、背景色「#000;」

自分が不要でも、World wide Webに25年も浸かってきた人間が余力あるなら、つけとけよ! てな感覚に変化した。 ただし自分は「真っ黒黒」が苦手で持ち腐れていたわけだが、今回は理想としていたー Light / Dark / Black ーの3択タイプにしたので、晴れて好みの色を設定できたダークモードを活用している。


情けは人の為ならず。
案外、居心地良い。編集画面とアウトプットで色が違う2画面とか、デュアルディスプレイ甲斐があるっ

色で判断したい獣タイプなんでね、丸の内線と銀座線が赤と黄色じゃなくなった長い期間・・・どれほど乗り越しと乗り間違えをやらかしたことか。

堕落してない人はきっとわからないでしょう。脳のキャパシティーを使わないためなら、けっこうマメに手も体も動かすんですよ怠惰なわたくしも。(何の話だ)


サンプル

Darkmode+3Type

このサイトとまったく同じものですが。ナビゲーションだけにしてあるソースが
studiomic.github.io/mode at Dark-Lighe-Black-mode

要件

  1. @media (prefers-color-scheme: dark) 対応はせずユーザーアクション主体(予定)
  2. 背景色 #000; == 完全ダークモードをBlackモードとして3択にする(擬似MDN)
  3. ページ毎動作ではなく設定保持でわずらわしくないのが良い(localStorage)
  4. 後日気が変わって 1.はOSカラースキームを開始点にした(初回のみ)

言うてしまえば、「use-dark-mode」プラグインで配置していたものより質落ちはNG。
理想は、MDN 以外では実例を見た事がないのだが・・・前々から「3択」にしたかった。

MDNの場合は

  • OS Default
  • Light
  • Dark

という3択になっており、OSはダークモードにしていても、Webはライトモードで読みたいときもある・・・という私のようなユーザーには垂涎もの。

とはいえ[OS Default]は昼夜で変わるオートモードにしていないかぎり、Light・Darkと重複するのが惜しい。


また先の「スターバースト現象」を知って、ユーザーエクスペリエンスを謳うならダークモードは背景「#000;」一択やないかーい。と思っていた流れで過去にも書いていたのを引用する。


外観としては、Codepenで見かけたLight / Dark / Black Themeが全員嬉しい感。

3ThemeMode

ダークモードでも発色ゼロの漆黒Blackじゃないと目にきびしいという方もいれば、私は逆に「#000000」は、きつすぎてエディターのテーマなどでも敬遠します。
「濃灰色」止まり。

このCodepenのをlocalStorage保存つきで実装できれば便利だと思うものの、書けない。


と、そのときは思ってプラグインを導入したわけだが、Gatsby Cloud → Netlify移転で元の木阿弥、まっさら白紙に戻ったこの機会に3択モードを実装することにした。

工程

  1. まずはVanilla JSでサクッと書き出し
  2. Gatsby BuildでlocalStorageの扱いがまずいわ、と叱られ対処
  3. React Hook(useEffect)の使い方を見直して、完成

と行くまでに、けっこう重要だったかも!なのが

外観はともかく、インプットをcheckboxにするかbuttonにするかで面倒くささが違う。
checkboxでもradioでも、状態が遷移するものは当然アウトプット用の返り値を書かないとならんし、特にcheckboxなど同じname属性を複数につけられるものは、For文で洗いださないと「動作」があった:という根拠にできないので更に面倒だ。と後で気づいた。

buttonは単に押されたかどうか、のみでトリガーにできるのが重宝。

という脱線から戻って、1.Vanilla JSでの書き方はわりと流通していると思う。
ナビゲーションを上部で固定、なんて時にも使う「後から当該エレメントにCSSクラスを加筆したり消去したり」しろよーというadd / remove


localStorageから設定キーの値を取り出す
let modeType = localStorage.getItem('mode');

if ( modeType === 'darkmode') {
  document.documentElement.classList.add("darkmode");
  document.documentElement.classList.remove("lightmode","blackmode");

} else if ( modeType === 'blackmode') {
  document.documentElement.classList.add("blackmode");
  document.documentElement.classList.remove("darkmode","lightmode");

} else if ( modeType === 'lightmode') {
  document.documentElement.classList.add("lightmode");
  document.documentElement.classList.remove("darkmode","blackmode");

} else {

}
  • 1行目でローカルストレージに値があるか変数:modeTypeへ代入し、🟰ダークモードなら、🟰ライトモードなら、とclassList.add設定/classList.remove消去を振り分けるのだが
  • document.bodyではなくdocument.documentElementに施している。bodyではなく htmlにclass付けする場合
html {
  background-color: var(--background);
  transition: background-color .5s ease;
}

bodyにbackground-color:は設定せず、htmlに置き換え用変数を設定しておく。


localStorageとは

localStorageは、Amazonの「最近見た商品」などにも使われているらしい・・・が、ここではたった一つのキーなのでブラウザのデベロッパーツールで一目瞭然。

アプリケーション:ストレージ:ローカルストレージ・(mode)Keyが
onClick={Light}・{Dark}・{Black}を押すたびに、
値(lightmode・darkmode・blackmode)と入れ替わる。

localStorage.clear(); で消去しないかぎり永続的なローカルストレージの下には、セッションストレージの文字が見えますね(画像左下)

SessionStorageは、別タブとは共有しないそうで、閉じたら終わり。


具体的な使用例はReact Hooks を使って localStorage のデータを保存・取得する方法という翻訳ページが秀逸。

localStorage は、JavaScript を用いて作られたサイトやアプリが、有効期限なしでウェブブラウザにキー・バリュー形式のデータを保存するためのウェブストレージオブジェクトです。

つまり、保存されたデータはページを更新したり、ブラウザを再起動しても残ったままです。これは、ブラウザに保存されたデータは、ブラウザウィンドウが閉じられても残ることを意味します。

設計変更

OSの設定🟰@media (prefers-color-scheme: dark) で対応する場合、毎度OSに合わせて表示し、後から変更できるトグル、でも十分だが。
最初に「ユーザーアクション主体」とした名残を残して、一度でもモード変更ボタンを押した場合はOSよりその値を優先、という書き方にした。

OS設定がダークモードかの判定をし、true;ならchangeDark処理
const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const changeDark = darkModeMediaQuery.matches;
if (changeDark) {
  document.documentElement.classList.add("darkmode");
}

localStorageから設定キーを参照して、「空」だった場合は上のコードでOS設定を反映する。
初回来訪者と、モード切り替え履歴のないユーザーがこれにあたる。


localStorage優先で、初回はOS設定を反映するIF文ネスト
useEffect(() => {
  let modeType = localStorage.getItem('mode');

  if ( modeType !== '') {

    if ( modeType === 'darkmode') {
      document.documentElement.classList.add("darkmode");
      document.documentElement.classList.remove("lightmode","blackmode");
    } else if ( modeType === 'blackmode') {
      document.documentElement.classList.add("blackmode");
      document.documentElement.classList.remove("darkmode","lightmode");
    } else {
      document.documentElement.classList.add("lightmode");
      document.documentElement.classList.remove("darkmode","blackmode");
    };

  } else {
    const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
    const changeDark = darkModeMediaQuery.matches;
    if (changeDark) {
      document.documentElement.classList.add("darkmode");
    }
  };
});

Gatsby Buildエラー回避に、useEffectを使用する

工程で、2.Gatsby BuildでlocalStorageの扱いがまずいわ、と叱られ対処 ... と書いた過程。

WebpackError: ReferenceError: localStorage is not defined

開発環境では問題ないが、上のエラーメッセージが出てBuildできない。

エラーメッセージをそのままググり

を読むも、所詮 { useEffect, useState } の役割をわかっちゃいないのでピンとこない。

先ほどの翻訳ページを読んで、少しずつ文章がちゃんと頭に入ってきたところで、付け焼き刃の対処を続けるとエラーメッセージも変化する。

"localStorage" is not available during server-side rendering.
 Enable "DEV_SSR" to debug this during "gatsby develop".

// 翻訳:
「localStorage」は、サーバー側のレンダリング中には使用できません。
「DEV_SSR」を有効にして、「gatsby」中にこれをデバッグします
開発する"。

このエラーの詳細については、ドキュメント ページを参照してください: https://gatsby.dev/debug-html

バカみたいに当たり前なこと

そう。localStorageはブラウザに保管され、参照したり上書きしたり、配列を使ったりできるもの。
所有者:Browserさん。
Node.jsの所有物じゃないんすよね。モジュールでも何でもないもの、Build中に触れるかーっ!

と言われたら、その通りだ。(かなりワタクシの性格的な意訳かかってるけど、)
ReferenceError: localStorage is not defined (ないものは、ない。参照しようもねぇ、)


ならぬものはならぬ。会津ですわねぇ。

GitHub Issueにあった「コンポーネントがマウントされた後にデータを取得するだけです。」も
Zenn記事曰くの「useEffectでlocalStorageへの参照のタイミングをずらす」
「エラーを回避する方法としてuseEffect内でlocalStorageを参照するように変更します。」

意味がわかるとご尤もすぎた。

useEffect内でlocalStorageを参照する
useEffect(() => {

  let modeType = localStorage.getItem('mode');
  // localStorage参照以降の処理をuseEffectの中に置く

  if ( modeType !== '') {
    // getItem('mode')に値があれば、はじまるIF
    if ( modeType === 'darkmode') {// Dark
    } else if ( modeType === 'blackmode') {// Black
    } else {// Light
    };
  } else {
    // getItem('mode')が空ならOSモード設定に合わせる
  }

});

先にやってみたのが、

const handleBeforeUnload = () => {
  console.log('beforeunload')
}
useEffect(() => {
  window.addEventListener('beforeunload', handleBeforeUnload)
  let modeType = localStorage.getItem('mode');
  .....

Reactのリロード処理。これを思いつくまで難航した。
・・・アラート出すやつですよね、普通は。 編集中のユーザーが離れようとしたら、このタブ閉じて大丈夫?みたいに訊いてくるやつ。

もっと細かく書かないとアラートは出ないのだが、閉じられる前に保存処理をするから、リロードかけられても先のmode値がlocalStorageに上書きされて「再び開く」

しかしコレ考え過ぎでuseEffectの中に置くだけで十分でした。
useEffectはマンウトしたときに一度だけ実行するもの、なのでブラウザで開いたときに一度だけgetItem('mode');する。この用途に「合っている」と気付けないのがアマチュア。


Buttonアクション

onClick={Light}の動作
let modeType;
  const Light = () => {
    document.documentElement.classList.add("lightmode");
    document.documentElement.classList.remove("darkmode","blackmode");
    modeType = "lightmode";
    localStorage.setItem('mode', modeType);
  };
// const Light = () => {...自分をADD、他2つをREMOVE}

5行目でmodeType(変数)へdarkmode(class名)を代入し、 localStorageに(key, value)形式で保存。

let modeType;は、 useEffect内で一度していても、再度ローカル変数宣言が必要。

Relodeアクションしてみるまでの右往左往

beforeunloadはさすがに獣道じゃないのか・・・と後日見直して、useEffect内で十分とわかったけども、Gatsbyは触るのが面白いので、失敗してもReact学習にもなって、時間を損した気がしないのが良い。
Reactの公式ドキュメントにいたっては日本語版も充実しているし、ありがたいですね。

あとはもう少しスマートなJSが書けたら良いのだけど。