React 関数コンポーネントでsetIntervalを使うには?

ぼくのReaction

こんにちは。

今回は、Reactの勉強で詰まったところを解決しましたので、シェアします。

現在僕は、Reactの勉強を「りあクト!」という書籍で取り組んでいます。

タイマーを実装することを通じて、reactの学習を進めていく段階でした。

僕は、書籍のサンプルに手を加えて、ストップウォッチを作りました。

(少しいじってみる的なワンステップが、自分のレベルを飛躍的に上げてくれると実感しました。)

もちろん「クラスコンポーネント」で。

学習を進めていくうちに、

「関数コンポーネント」が登場しました。

いろいろな観点から、なるだけ関数コンポーネントで!ということなので、

クラスコンポーネントで作ったストップウォッチを関数コンポーネントで書き換えてみることにしました。

実際の画像はこんなところ。

  • Startで始まり、Stopで止まる。
  • Resetで60秒に戻る。
  • Startを何回押しても、平気!(連打すると暴走するsetIntervalのあれは防ぐ)
  • semantic-uiを使用。
  • TypeScript!

内容は以上です。

早速、クラスコンポーネントでのコード。(これは解読不要です!要点解説するので!)

interface TimerState{
  timeLeft: number;
  isRunning: Boolean;
}


class Timer extends Component<{}, TimerState>{
  constructor(props: {}){
    super(props);
    this.state = {
      timeLeft: LIMIT,
      isRunning: false,
    };
  }

  reset = () => {
    this.setState({timeLeft: LIMIT});
  };

  tick = () => {
    this.setState(prevState => ({timeLeft: prevState.timeLeft - 1}));
  };

  start = () => {
    if(this.state.isRunning === true){
      return;
    }
    this.timerId = setInterval(this.tick, 1000);
    this.setState({ isRunning: true });
    
  }
  componentDidUpdate = () => {
    const {timeLeft} = this.state;
    if(timeLeft === 0){
      this.reset();
    }
  };

  stop = () => {
    clearInterval(this.timerId as NodeJS.Timer);
    this.setState({ isRunning: false });
  }

  timerId?: NodeJS.Timer;

  render() {
    const {timeLeft} = this.state;

    return(
      <div className="container">
        <header>
          <h1>タイマー</h1>
        </header>
        <Card>
          <Statistic className="number-board">
            <Statistic.Label>time</Statistic.Label>
            <Statistic.Value>{timeLeft}</Statistic.Value>
          </Statistic>
          <Card.Content>
            <Button id="start" color="blue" fluid onClick={this.start} >
              <Icon />
              Start
            </Button>
            <Button id="reset" color="red" fluid onClick={this.reset}>
              <Icon name="redo" />
              Reset
            </Button>
            <Button id="stop" color="green" fluid onClick={this.stop}>
              <Icon></Icon>
              Stop
            </Button>
          </Card.Content>
        </Card>
      </div>
    );
  }

}

長くなっていますが、一つずつ解説します。

要点解説 クラスコンポーネントの方

①stateは残り時間を示す「timeLeft」と、タイマーが走っているのかを判別する「isRunning」

②ruturn()には、写真のようにHTMLを返すJSXが書かれている。

③(16行目)reset関数はtimeLeftを60に戻す。

④(20行目)tick関数はtimeLeftを1秒毎に減らすsetInterval!『ここが関数コンポーネント化でてこずった!』

⑤(24行目)start関数は、setIntervalを開始する。そして、isRunningがtrueならreturnする

⑥(39行目)stop関数は、cleatInterval!

⑦(32行目)componentDidUpdateはコンポーネントが変更されるごとに呼び出されるもので、timeLeftが0になったら60に戻してくれる。

以上です

関数コンポーネント化で詰まった点

完成形がこちら。↓

const Timer: FC = () => {
  const [timeLeft, setTimeLeft] = useState(LIMIT);
  const [isRunning, setIsRunning] = useState(false);
  const [timerId, setTimerId] = useState();

  const reset = () => {
    setTimeLeft(LIMIT);
  };

  const start = () => {
    if(isRunning === true){
      return;
    }
    setIsRunning(true);
    setTimerId( setInterval(tick, 1000) );
  }

  const stop = () => {
    clearInterval(timerId);
    setIsRunning(false);
  }

  const tick = () => {
    setIsRunning(true);
    setTimeLeft( prevTime => (prevTime === 0 ? LIMIT : prevTime - 1));
  };


  return(
    <div className="container">
      <header>
        <h1>タイマー</h1>
      </header>
      <Card>
        <Statistic className="number-board">
          <Statistic.Label>time</Statistic.Label>
          <Statistic.Value>{timeLeft}</Statistic.Value>
        </Statistic>
        <Card.Content>
          <Button color="red" fluid onClick={reset}>
            <Icon name="redo" />Reset
          </Button>
          <Button color="green" fluid onClick={start}>
            <Icon name="redo" />Start
          </Button>
          <Button color="blue" fluid onClick={stop}>
            <Icon name="redo" />Stop
          </Button>
        </Card.Content>
      </Card>
    </div>
  );
}

このコードでは、state(厳密には違うのかも)として、「timerId」を追加しています。

スコープ問題を解決するためです。

クラスコンポーネント版では、start関数内で、setIntervalの戻り値をstateに入れています。

それを、stop関数内で、clearIntervalの引数として受け取っています。

それが可能な理由は、

this.timerのスコープが、それぞれの関数を覆っているからです。

同じことを関数コンポーネントで実現しようとすると、

  • stateの概念がないので、どこにsetIntervalの戻り値を格納すればいいのか。
  • 格納した戻り値をclearIntervalの引数に使えるスコープに配置できるか。

と言った障壁に阻まれました。

ですので、useStateで新しい変数を作り、それにsetIntervalの戻り値を入れることにしました。

そうすることで、start関数からもstop関数からも変数にアクセスできるようになります。

クラスコンポーネント版で実装したように、setIntervalを関数コンポーネントでも使うことができました。

こんなやり方もあるのかもしれない。。。道半ばです。

思いつけば簡単だなと、あっさり納得できてしまいましたが、

これに至るまで、React初心者ましてやプログラムも初心者の自分は回り道をしました。

現状では、componentDidUpdateとcomponentDidMountを別々に処理するHooksは存在しないようです。(一緒にまとまって扱われる!!)

そこで、componentDidUpdateを関数コンポーネントでも使えるようにするために

useRefを使う。。とか

があるようです。

いろんな記事を漁りましたが、僕にはまだまだ理解できませんでした。

道半ばであると気づかされたと同時に、小さいながらもプログラムを自分で考えられた喜びを感じました。

わかりにくいところもあるかと思いますが、同志の手助けになれば幸いです。

コメント

タイトルとURLをコピーしました