🐕

かわいい犬がタスクを食べてくれるWebアプリを作った話

2024/06/03に公開

ご紹介

「タスクを記録すると、片っ端からかわいい犬が食べてしまうWebアプリ」を思いついたので、Next.js + Tailwind CSS の練習を兼ねて形にしてみました。

公開したWebアプリはこちら
https://faveo-systema.net/to-dog-list

Githubリポジトリはこちらです
https://github.com/Daiius/to-dog-list


スクリーンショット

思い入れのあるポイント

1.ダークモード対応

next-themesを用いた切り替え機能、意外とちゃんと実装するのは結構難しいのですね
(まだコンソールにエラーが出ます...)

2.画像の逐次事前ロード

次に表示する予定の犬の画像のみを読み込み、それを表示し終わったらすぐに次の画像を読み込みます
Next.jsの機能の一つであるnext/imageを使いましたが、priorityとやらを指定しないと上記の仕組みは逆効果になる場合もあるようです、難しいですね

3.犬の画像の向きをそろえるオプション

こういうオプション機能を作るのが好きです
OFFだと後ろ向きに走るのが好きな犬たちがいるのにお気付きになると思います

localStorageにお気に入りの設定が保存されるようにしました
ですがNext.jsの制約なのでしょうか、useEffect内でしか触れないため少し苦労しました

next-themesがどうやって設定を記憶しているのか疑問だったのですが、このオプションのデバッグ中にlocalStorageを使っているのに気付きました

(規模の小さなSPAで、クライアント側の処理で全て完結していますので、実はNext.jsを使う必要は無いんですよね...)

思い入れのあるポイント解説

1.ダークモード対応について

next-themesを使いました
Tailwind CSS を使用する際の注意書きがあったので、それに従っています

サーバ側でのレンダリングにも対応している優れものの様なのですが、
クライアント側の状態を一度サーバ側に伝えるためでしょうか、テーマ切り替え用コンポーネントにはひと工夫必要とのことです。

ThemeSwitch.tsx
import React from 'react';
import { useTheme } from 'next-themes'

const ThemeSwitch: React.FC = () => {
  const { theme, setTheme } = useTheme()
  // 以下の処理を追加で行います
  // 一度コンポーネントが描画されるまで待つことで、
  // サーバ側とテーマ情報のやりとりを行うようです
  const [mounted, setMounted] = React.useState<boolean>(false);
  useEffect(() => setMounted(true), []);

  if (!mounted) return null;
  // 追加の処理ここまで

  return (
    <select value={theme} onChange={e => setTheme(e.target.value)}>
      <option value="system">System</option>
      <option value="dark">Dark</option>
      <option value="light">Light</option>
    </select>
  )
}

export default ThemeSwitch

参考にした記事:
https://zenn.dev/hayato94087/articles/a340bfe21879a9

2.画像の逐次事前ロードについて

犬の画像を切り替えるのに、以下の処理を行っています

MainContainer.tsx
import Image from 'next/image';
//(省略)
const MainContainer: React.FC = () => {
  const [dogIndex, setDogIndex] = React.useState<number>(0);
  // (省略)
  <Image
    //(省略)
    alt='cute dog'
    width={150}
    height={150}
    src={'/to-dog-list/dogs/' + dogData[dogIndex].fileName}
    priority // ← 実は重要らしいです
  />
  // (省略)
  <TaskInput
    //省略
    onAddTask={newTask => {
      // タスクの追加処理(省略)
      if (tasks.length === 0) {
        // タスクが 0個 → 1個 になったときのみ
        // 犬を呼び出してアニメーションします
        // (事前にCSSでtransitionを指定したclassNameを付与します)
        // **アニメーション終了後、表示する犬画像を切り替えます**
        setTimeout(() => setDogIndex(dogIndex + 1), 5000);
      }
    }}
  />
}

export default MainContainer;

初期状態or犬がタスクを食べてしまった後のタイミングで画像がサーバから読み込まれます
通信速度が遅くて次の画像の読み込みが間に合わないと(滅多に無いですが...)、犬が走っている間に違う犬に切り替わってしまいます

最初はpriorityを付けないでいましたが、最初の犬画像が表示されるときのみ2回画像がロードされて表示が遅くなる様な現象が発生したので、このディスカッションを元に追加しました
https://github.com/vercel/next.js/discussions/21294
(何が変わるのでしょうか...ちょっと良くわかっていません、next/imageでロードされる画像がどの程度の時間保持されるかというオプションもあるらしいです...)

3.犬の画像の向きをそろえるオプションについて

犬の画像の向きを画像ファイル名と組みにして記録しています
この設定は人によって好みがあるかもしれませんので(?)、localStorageに保存して、ブラウザを閉じたりリロードしたりしても以前の設定を引き継げるようにしています

Reactの制限なのかNext.jsの制限なのか、localStorage変数はuseEffect内でしかアクセスできないようです...
以下の様なフックを用いて必要な処理をまとめてみましたが、既存の値があるかどうかの判定や、有れば読み取り、なければ規定値の使用など意外と神経を使います...
https://github.com/Daiius/to-dog-list/blob/main/src/hooks/useLocalStorage.ts
useLocalStorageを内部で使用するSettingsProviderによって、アプリ全体に関係する設定を保存しておきます
https://github.com/Daiius/to-dog-list/blob/main/src/providers/SettingsProvider.tsx
SettingsProviderが初めてマウントされるタイミングをuseMountフックで確認しています
これをuseLocalStorageに渡して既存の値があるかチェックし、useEffect(() => { /*省略*/ }, [])内でstateに写し取ります
https://github.com/Daiius/to-dog-list/blob/main/src/hooks/useMount.ts
useMountを親子関係にある複数のコンポーネントで呼び出すと、子コンポーネントの再描画回数が増えそうなのが気になって、アプリ全体で1度だけ呼び出す様に工夫しています
SettingsProvidermountedという変数を返すのは不自然ではあるのですが...)

この様にしてlocalStorageも使用して設定値がコンポーネントに渡されて、描画方法が変更されます。

おわりに

次はかわいい犬に食べられないTo-Do Listを作ります!

Discussion