1. 概要
Reactを実装していると、複数のstateが相互に依存するシチュエーションに遭遇することがあります。このような場合、適切な方法でstateを管理しないと、コードが複雑になり、バグの温床になることがあります。ここでは、依存関係のあるstateを管理するベストプラクティスについて説明します。
2. 内容
2-1. Bad Code
依存関係のあるstateを独立して定義し、それらをuseEffect
で更新する方法は推奨されません。
例えば、商品の値段と最終的な会計金額を扱う以下のようなコードがあったとします。
badCode.jsx
const PriceCalculator = ({ basePrice, taxRate }) => {
const [totalPrice, setTotalPrice] = useState(0);
const [finalPrice, setFinalPrice] = useState(0);
useEffect(() => {
setTotalPrice(basePrice * (1 + taxRate));
}, [basePrice, taxRate]);
useEffect(() => {
// 10%のチャージを加算
setFinalPrice(totalPrice + (totalPrice * 0.1));
}, [totalPrice]);
return (
<div>
<p>Base Price: {basePrice}</p>
<p>Total Price: {totalPrice}</p>
<p>Final Price: {finalPrice}</p>
</div>
);
このコードは以下の3つの問題点を抱えています。
- 同期の問題
-
useEffect
はレンダリング後に非同期的に実行されるため、依存関係のあるstate間での更新が即時に反映されない可能性があります。例えば、totalPrice
が更新された後にfinalPrice
が更新される場合、useEffect
の順序によっては以前の値を元に計算されることがあり、意図しない値が表示されることがあります。
-
- 冗長なコード
- 複数の
useEffect
フックを使用して依存関係のあるstateを管理すると、各useEffect
がどのstateに依存しているかを個別に記述する必要が出てきます。これにより、依存関係が複雑になると、それぞれのuseEffect
内で似たような更新ロジックを何度も書く羽目になります。例えば、totalPrice
が変わるたびにfinalPrice
を更新するようなロジックを複数のuseEffect
で記述する必要が出てきます。これが原因でコードが冗長になり、どのuseEffect
がどのstateを更新するのかが分かりにくくなります。
- 複数の
- バグのリスク
- stateを手動で同期させる際に、更新のタイミングや順序が予期しない形で影響を与えることがあります。例えば、
setTotalPrice
が呼ばれた後にsetFinalPrice
が呼ばれる場合、totalPrice
の更新が完了する前にfinalPrice
の計算が始まることがあり得ます。その結果、finalPrice
の計算が正しいtotalPrice
の値に基づかず、古い値に基づいてしまうことがあります。これが原因で、正しい最終価格が表示されず、ロジックの整合性が崩れます。
- stateを手動で同期させる際に、更新のタイミングや順序が予期しない形で影響を与えることがあります。例えば、
2-2. Good Code
上述の問題に対し、useMemoを使うことで依存関係のあるstateを効率的に管理することができます。
goodCode.tsx
const PriceCalculator = ({ basePrice, taxRate }) => {
const totalPrice = useMemo(() => {
return basePrice * (1 + taxRate);
}, [basePrice, taxRate]);
const finalPrice = useMemo(() => {
// 10%のチャージを加算
return totalPrice + (totalPrice * 0.1);
}, [totalPrice]);
return (
<div>
<p>Base Price: {basePrice}</p>
<p>Total Price: {totalPrice}</p>
<p>Final Price: {finalPrice}</p>
</div>
);
};
(問題点の裏返しにはなりますが)これにより以下の点で改善が期待されます。
- 可読性の向上
- サンプルコードとして
totalPrice
とfinalPrice
という、あえて微妙な変数名をつけていますが、useMemoで定義することで、名前が本来の責務を表現できていなかったとしても、それらの算出ロジックが定義場所から読み取れるため、実装者がそれぞれの役割を推察しやすくなります。
- サンプルコードとして
- 明確な依存関係
-
useMemo
を使うことで、計算された値がどの依存関係によって更新されるかを明示的に管理できます。これにより、依存関係が変わったときのみ再計算が行われ、無駄な再レンダリングを避けることができます。
-
- パフォーマンスの向上
-
useMemo
は、依存している値が変更された場合のみ再計算を行うため、パフォーマンスが向上します。必要なときにのみ計算が行われることで、不要な計算を避け、アプリケーション全体のレスポンスが向上します。
-
3. まとめ
useState,useEffectはreactを代表するhooksである一方で、使い方によってはコードの複雑性をあげバグの温床にもなり得るものなので、気軽に定義せず、本当に必要なのか一考することをお勧めします。