keyを有効活用してuseEffectの利用を減らす
トラストハブの松清です。
useEffect
はReactにおいてとても便利な機能です。しかし、その柔軟性が故にコードが複雑化しやすいという課題があります。以下のような経験をされた方も多いのではないでしょうか?
- コンポーネント初期開発時に、
useEffect
を1箇所だけ呼び出す。最初はシンプルなつくりであったため問題は起きなかった。 - 機能追加・仕様変更が積み重なる。
- コンポーネント内にいくつも
useEffect
が登場し、それらが複雑に絡み合うようになる。コンポーネント全体の挙動を把握するのが難しくなってくる。初期から開発に参加していたメンバーであれば、ギリギリ把握できるというレベル。 - 開発に新規参入したメンバーが挙動を理解しきれず、
useEffect
起因のバグが発生。 - その結果、コードレビュー時に
useEffect
が使われているのを見ると「ここでバグが起きないだろうか?」と過度に慎重にならざるを得なくなる。レビューの負担や時間が増加する。
以前、弊社のフロントエンドのコードでも、状態リセットのようなシンプルな用途でも useEffect
を使っている場面がよく見られました。現在はコードの見直しを進めており、key
を有効活用することで useEffect
に頼らない実装へと置き換えています。本記事では、その具体的な方法について紹介します。
useEffect
を使っている例
状態リセットに以下のようなECサービスの管理画面を考えてみます。
-
画面左側にはユーザーからの注文一覧を表示するテーブルがある
-
行をクリックして注文を選択すると、画面右側にその注文に関する詳細情報が表示される
-
詳細情報エリアの上部には「追跡番号を入力」ボタンがあり、ボタンを押すとその注文について商品発送を行う際の追跡番号を入力できる
Claudeに作ってもらったUIイメージ画像
useEffect
を使ってこの処理を書くと、以下のようになります。
ある注文に対して追跡番号を入力した後、別の注文を選択した際に前回入力した値が残ってしまうことを防ぐために、useEffect
を使ってtrackingNumber
をリセットしています。
export const OrdersTable = () => {
// クリックされた行をこのStateに保存する
const [selectedOrder, setSelectedOrder] = useState();
return (
<>
{/* 画面左側に注文一覧を表示するテーブルがある */}
<Table>
<Thead>...</Thead>
<Tbody>
{orders.map(order => (
(<Tr>{...order}</Tr>)
)}
</Tbody>
</Table>
{/* 画面右側 */}
{/* クリックされた注文の情報を表示するコンポーネント */}
<OrderDetail order={selectedOrder}/>
</>
)
}
export const OrderDetail = (props) => {
const { order } = props;
const [trackingNumber, setTrackingNumber] = useState('');
// propsのorderが変更されたらStateをリセット
useEffect(() => {
setTrackingNumber('');
}, [order]);
return (
<>
{/* 注文情報を表示 */}
<Text>発送先: {order.shippingAddress}</Text>
{order.items.map(item => (
(<Tr>{...item}</Tr>)
)}
{/* 追跡番号を入力するボタン */}
<Button>追跡番号を入力</Button>
{/* 追跡番号を入力するフォームを表示するモーダル*/}
<Modal>
<Form>
...
</Form>
</Modal>
</>
)
}
key
を使った改善例
先ほどの例では、useEffect
を利用してtrackingNumber
をリセットしていましたが、コンポーネント呼び出し時にkey
属性を設定することで、selectedOrder
が変更されたときにコンポーネントが再生成されtrackingNumber
が自動的にリセットされます。
// 呼び出し側でkeyを設定
// selectedOrderが変更された際にkeyの値が変わる
<OrderDetail key={selectedOrder.id} order={selectedOrder}/>
export const OrderDetail = (props) => {
const { order } = props;
const [trackingNumber, setTrackingNumber] = useState('');
// useEffectは不要になるので削除
return (<></>);
}
この方法は公式docsでも推奨されている方法で、以下のようなメリットが得られることが紹介されています。
-
レンダリングの効率化
useEffect
を使う場合は、渡されるpropsが変更された際に1回レンダリングを行った後にuseEffect
の実行に伴って再度レンダリングを実行します。つまり、合計2回のレンダリングが行われることになります。key
を使えば、レンダリングが1回で済みます。 -
ネストされたコンポーネントの扱いが楽になる
useEffect
を使う場合、OrderDetail
内で呼び出されているコンポーネント内のstateについても初期化処理を行う必要があり、対応はより複雑になります。key
を使えば、ネストされたコンポーネントも全て初期化されるので対応が楽です。
keyを使う際の注意点
key
は一意である必要があります。ここでは各orderに一意なIDを割り振っていることが前提となっています。もし、IDが重複する可能性がある場合はユニークな値を生成して渡すようにする必要があります。
まとめ
key
を有効活用することで、useEffect
の多用を避け、コードをより簡潔かつ効率的な状態にすることができます。「useEffect
の多用でコードが複雑化している」と感じている人はぜひ活用を検討してみてください。
Discussion