Torihaji's Growth Diary

Little by little, no hurry.

久しぶりにReactのカウントダウンタイマー作ったら学びが深かった話

はじめに

どうもこんばんちは、torihaziです。

フリーとして独立してからはや2ヶ月ほど。

あっという間に涼しくなって、感覚がバグっております。

話変わって、今回はReactのカウントダウンタイマーを作ってみました。

ことの発端は CLI100本ノックとかをRustでやってたんですけど、

そのノリとおんなじでそのReactバージョンて感じですね。

Next.jsも根っこはReactだし、正直キャッシュの使い方とかそこくらいしか違いないと思ってる(笑))

とにかくReactわかんなきゃ、Next.jsも使いこなせんだろうということで

今回は振り返りということも兼ねて、作っていくことにしました。

あとは最近のAI使いすぎ脳を叩き直す面も兼ねてます。

あと最後の結論が待ってるとかそういう感じで書いてないです。

そもそも

作る対象は簡素なものです。

カウントダウンタイマーです。装飾とかは一切気にせず機能が以下であるようなものです。

  • ある設定値の状態でボタンを押したら、1秒ずつ表示が減っていく

ていう感じです。

じゃあやろう。

となるわけですが、どう作りますか。

方針です。AIとかに聞かずに考えてください。


私はここで詰まりました。

これは知らないと無理な感じもします。

それがsetIntervalという関数です。

使い方は色々あるみたいですが、今回は

setInterval(fn, delay) を使います。

これだと 指定したdelay(ms)ごとにfnが実行されます。setIntervalは 識別子として正の整数を返します。


じゃあ次のフェーズ。setIntervalを使ってどうしようということですが、

そもそも作るカウントダウンタイマーは「1sごとに値を減らす」ことができれば合格です。

この考えを先ほどのsetIntervalに当てはめると

setInterval( {値を1減らす処理}, 1000}

という感じに書けば良さそうです。

そして、この値をアプリケーションで記憶しておく必要があります。

Reactで状態を記憶すると言ったら「useState」なのでそれを使いましょう。

useRefも「状態を記憶する」という目的では良いですが、公式読んだ感じ何かで不適切だったので

気になる人いたら後で調べて教えてください。

なので結局 setTimer(time -1)みたいなことをするんだろうなという結論ですね。

useEffect

ここでそもそもReactとはなんだったのかを確認します。

ReactはUIライブラリです。文字のごとく、UIの管理に使うべきものです。

機能で色々、UI以外にもできることがありますが、とにかくUIの管理専門です。

そのため、本来ならUIの管理に関係ないことは 扱うべきではありません。

今回のような時間の管理などはまさにその対象です。

このような時に使うものでuseEffectがありましたね。

自分はuseEffectは副作用が云々〜〜という説明、嫌いというか「副作用ってなんやねん」人間なので

個人的な認識を話します。ただ正直現在もしっくりきているものではないのでupdateはしていきたいです。

useEffectは UI管理に関係ないに使う て感じです。

で、今回どう使うかというと、そのタイマーの管理ですね。

でこいつは色々特徴がありまして、

  • クライアント上で実行される
  • クリーンアップしっかりしなさい
  • 依存配列ロジックとして使うなよ

みたいなことがあります。詳しくは以下

あとはひたすら公式を読んでください。

でuseEffect使った結果がこれですね。

const [time, setTime] = useState(10)

   useEffect(() => {
     const id = setInterval(() => {
       setTime(time - 1);
     }, 1000);
     console.log(id);
     return () => clearInterval(id);
   }, [time]);


return <div>{time}</div>

みたいな感じですね。これで動きます。

仕組みとしては

  • 初回レンダリング(state 10)
  • effect実行
  • idを定義して、timerに関数登録
  • 1s後、setTime(time-1)が実行
  • stateが変わったので再レンダリング
  • 依存配列を見て、timeが変わったので、クリーンアップされてから、再度timer登録

てな感じです。

忘れがちなクリーンアップやらないとメモリリーク起こします。

要はゴミが残ります。

で、

これで動くんですが、よくないっぽいですねこれ。

よくない理由としては、毎秒 timer登録をするということ。

じゃあ依存配列のtime外せばいいやんとなりますが、それだと動きません。

   useEffect(() => {
     const id = setInterval(() => {
       setTime(time - 1);
     }, 1000);
     console.log(id);
     return () => clearInterval(id);
   }, []);

なぜか。初回実行時のtime=10を参照し、最初の処理で - 1をされてstateが変わって

レンダリングされても依存配列にそのtimeが入ってないので、実行されず、

結局 9 で止まります。じゃあ次の一手として、set関数で現在の値に干渉し、setするというやり方で

setState((prev) => prev - 1)みたいな書き方がありました。

これを使うと、良さそうです。

   useEffect(() => {
     const id = setInterval(() => {
       setTime(prev => prev - 1);
     }, 1000);
     console.log(id);
     return () => clearInterval(id);
   }, []);

ただここでも問題が。

0に達した時に -1されるので負の値でも続きます。

そのため分岐をします。

  // useEffect(() => {
  //   const id = setInterval(() => {
  //     setTime((prev) => (prev > 0 ? prev - 1 : 0));
  //   }, 1000);
  //   return () => clearInterval(id);
  // }, []);

こんな感じで書いても 0の時は 0で更新し続けるという挙動なのでよくないです。

  // useEffect(() => {
  //   const id = setInterval(() => {
  //     setTime((prev) => {
  //       if (prev <= 1) {
  //         clearInterval(id); // ここで止める!
  //         return 0;
  //       }
  //       return prev - 1;
  //     });
  //   }, 1000);

  //   return () => clearInterval(id);
  // }, []);

忘れずに 分岐の中でclearIntervalしておきましょう。

次のいって。

ここからstartボタンとか時分秒対応しようとかは以下です。

startボタンについてはstateを新しく増やす感じで、後者は 秒で管理して、表示する時に変換するというやり方でいけるそうです。

個人的に「秒で管理して、表示だけ変える」というのは発見でした。

githubにもしたのでよければ。

結論

学びが深かったというタイトルなのでなんの学びが得られたかというのを簡単に。

  • useEffectは UI管理以外に必要なものに使うこと
  • setIntervalは指定秒間隔で実行したい処理がある時に使用可能で、識別子として整数を返す
  • useEffectは 必ず clean upできるものはしてね。
  • 依存配列が変わったら、まずクリーンアップしてからメインの関数が実行されるよ。
  • useEffectの使い所むずいなー。
  • AI使いすぎはあかんなー。