
こんにちは。
今回は、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を使う。。とか
があるようです。
いろんな記事を漁りましたが、僕にはまだまだ理解できませんでした。
道半ばであると気づかされたと同時に、小さいながらもプログラムを自分で考えられた喜びを感じました。
わかりにくいところもあるかと思いますが、同志の手助けになれば幸いです。
コメント