Reactを勉強し始めたときに必ずと言って良いほど「カウントアップ」を作りますよね。
自分も同じくカウントアップから勉強したのですが、意外と深堀りせずに次のステップに進んでしまっていました。
「useState」の扱いには少しばかり注意が必要で、自分もハマったことがあるので共有です。
また動画も用意しているのでお好みでどうぞ。
意外とやりがちなuseStateのミス
まずネットで調べて辿り着くカウントアップの作り方を紹介します。
初心者向けに公開されている例の大半が以下のように作られています。
import React, { useState } from "react";
const Count = () => {
const [count, setCount] = useState(0);
const handleCount = (num) => {
setCount(count + num);
};
return (
<div>
<div>
<button onClick={() => handleCount(-1)}>-</button>
<button onClick={() => handleCount(+1)}>+</button>
<p>{count}</p>
</div>
</div>
);
};
export default Count;
カウントの数を「useState」で管理しているシンプルなプログラムですね。
こちら先に言っておくと別に何も問題はありませんし、正常に動作することには間違いありません。
多くの学習教材では全てと言って良いほど上記のコードで共有されていて、自分も全くもって同じ書き方で始まりました。
しかしカウントアップのみのアプリだから良いのですが、他の機能も含めた全体としては少しばかり怖い点があります。
useStateで管理するステートは書き方をミスるとハマる
先ほどのコードに少しばかり意地悪な修正をしてみたいと思います。
クリックしたときの関数を下記のようにダブルにしてみましょう。
import React, { useState } from "react";
const Count = () => {
const [count, setCount] = useState(0);
const handleCount = (num) => {
setCount(count + num);
setCount(count + num); //こちらを追加して処理をダブルにする
};
return (
<div>
<div>
<button onClick={() => handleCount(-1)}>-</button>
<button onClick={() => handleCount(+1)}>+</button>
<p>{count}</p>
</div>
</div>
);
};
export default Count;
何も知らない自分は単純に±2されると思っていました。
しかし実際には先ほどと同じく±1しかされません。
エラーではないのですが思ったような動作は実現できていないのです。
理由は後で解説するので先に進みます。
ステート関数は出来るだけ関数型でやろう
続いて下記のように修正してみます。
import React, { useState } from "react";
const Count = () => {
const [count, setCount] = useState(0);
const handleCount = (num) => {
setCount((currentCount) => {
return currentCount + num;
});
setCount((currentCount) => {
return currentCount + num;
});
};
return (
<div>
<div>
<button onClick={() => handleCount(-1)}>-</button>
<button onClick={() => handleCount(+1)}>+</button>
<p>{count}</p>
</div>
</div>
);
};
export default Count;
同じく処理をダブルにしているのですが、setCountの書き方が変わっています。
ステート変数のcountを単純に足す、引くのではなく、関数の形にして引数にcurrentCountを取ってcurrentCountとの計算をreturnで戻り値として出すようにしています。
「前の数字をもとにして計算する」ことをやっているのです。
こちらにすると期待通り±2の動きになります。
動作部分については冒頭に動画を添付しているので気になる方は見てみてください。
先ほどとの書き方だと「ステートを上書き」することになってしまうんですね。
とはいえ±2をしたいのであれば、handleCountの引数に2を渡せば済むので上記のコードは実際に使うことはないでしょう。
今回お伝えしたかったのは、出来ることならステート関数は「関数型」で書こう、ということです。
関数型にすることで引数と戻り値で「値を上書きするか、引き継ぐか」が明確になりますし、後からの追加実装にも少しばかり耐えやすくなります。
Reactで作るカウントはレンダリングしないことがある
もう1例のよくあるミスを紹介します。
基本的にはカウントをするたびにレンダリングが走るカウンターですが、以下のようなコードにすると思ったところでレンダリングされないのをご存知でしょうか。
import React, { useState } from 'react';
import Counter from './Counter';
const WrongCounter = () => {
const [isAdmin, setIsAdmin] = useState(true);
return (
<div>
{isAdmin ? (
<Counter role="Admin"/>
) : (
<Counter role="staff"/>
)}
<button onClick={() => setIsAdmin((prev) => !prev)}>スイッチ</button>
</div>
);
};
export default WrongCounter;
import React, { useState } from "react";
const Counter = ({ role }) => {
const [count, setCount] = useState(0);
const handleCount = (num) => {
setCount((currentCount) => {
return currentCount + num;
});
setCount((currentCount) => {
return currentCount + num;
});
};
return (
<div>
<h1>{role}</h1>
<div>
<button onClick={() => handleCount(-1)}>-</button>
<button onClick={() => handleCount(+1)}>+</button>
<p>{count}</p>
</div>
</div>
);
};
export default Counter;
スイッチとクリックしてタイトルがAdminからStaffに切り替わったのにカウントの数字がリセットされていません。
エラーというわけではないのですが、上記のような画面ではタイトルを切り替えたらカウントがリセットされて0になることを望む場合が多いです。
実はpropsでタイトル名を渡しているのですがReactはpropsの差異をレンダリングの参考にはしていません。
Reactが参考にしているのはテキストやHTMLタグなどマークアップの差異を監視してレンダリングするか決めています。
上記のコードだとカウンターを担当するCounter.jsというコンポーネントを2つ用意して、タイトルだけをpropsで分けている状態です。
そうするとpropsが変わったとしても同じCounter.jsなのでReactとしては「マークアップが変わっていないからレンダリングしない」という判断になるのです。
意外と知らない人が多いので注意です。
じゃあどうするかと言うとmapメソッドでも使用するkey属性を付与することです。
import React, { useState } from 'react';
import Counter from './Counter';
const WrongCounter = () => {
const [isAdmin, setIsAdmin] = useState(true);
return (
{/* Counterにkeyを付与する */}
<div>
{isAdmin ? (
<Counter role="Admin" key="Admin"/>
) : (
<Counter role="staff" key="Staff"/>
)}
<button onClick={() => setIsAdmin((prev) => !prev)}>スイッチ</button>
</div>
);
};
export default WrongCounter;
key属性はpropsと違って特殊なプロパティで「HTMLに目印をつける」と言う役割を持っています。
そのため同じCounterコンポーネントでも目印が変われば「マークアップが変わったからレンダリングする」ということになるのです。
mapメソッドを使うときに慣習的につけているkey属性にもしっかり意味があったわけですね。
実際に動かしてみると思ったような動作になります。
カウントアップのような簡単なプログラムほど、「簡単に終わらせたい」と思ってしまうところですが実務を想定した書き方を常に意識するように自分への戒めを込めて共有させて頂きました。