🔖

keyを有効活用してuseEffectの利用を減らす

2024/11/25に公開

トラストハブの松清です。

useEffectはReactにおいてとても便利な機能です。しかし、その柔軟性が故にコードが複雑化しやすいという課題があります。以下のような経験をされた方も多いのではないでしょうか?

  1. コンポーネント初期開発時に、useEffectを1箇所だけ呼び出す。最初はシンプルなつくりであったため問題は起きなかった。
  2. 機能追加・仕様変更が積み重なる。
  3. コンポーネント内にいくつもuseEffectが登場し、それらが複雑に絡み合うようになる。コンポーネント全体の挙動を把握するのが難しくなってくる。初期から開発に参加していたメンバーであれば、ギリギリ把握できるというレベル。
  4. 開発に新規参入したメンバーが挙動を理解しきれず、useEffect起因のバグが発生。
  5. その結果、コードレビュー時にuseEffectが使われているのを見ると「ここでバグが起きないだろうか?」と過度に慎重にならざるを得なくなる。レビューの負担や時間が増加する。

以前、弊社のフロントエンドのコードでも、状態リセットのようなシンプルな用途でも useEffect を使っている場面がよく見られました。現在はコードの見直しを進めており、key を有効活用することで useEffectに頼らない実装へと置き換えています。本記事では、その具体的な方法について紹介します。

状態リセットにuseEffectを使っている例

以下のようなECサービスの管理画面を考えてみます。

  • 画面左側にはユーザーからの注文一覧を表示するテーブルがある

  • 行をクリックして注文を選択すると、画面右側にその注文に関する詳細情報が表示される

  • 詳細情報エリアの上部には「追跡番号を入力」ボタンがあり、ボタンを押すとその注文について商品発送を行う際の追跡番号を入力できる

    Claudeに作ってもらったUIイメージ画像

    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でも推奨されている方法で、以下のようなメリットが得られることが紹介されています。

https://ja.react.dev/learn/you-might-not-need-an-effect#resetting-all-state-when-a-prop-changes

  1. レンダリングの効率化

    useEffectを使う場合は、渡されるpropsが変更された際に1回レンダリングを行った後にuseEffectの実行に伴って再度レンダリングを実行します。つまり、合計2回のレンダリングが行われることになります。keyを使えば、レンダリングが1回で済みます。

  2. ネストされたコンポーネントの扱いが楽になる

    useEffectを使う場合、OrderDetail 内で呼び出されているコンポーネント内のstateについても初期化処理を行う必要があり、対応はより複雑になります。keyを使えば、ネストされたコンポーネントも全て初期化されるので対応が楽です。

keyを使う際の注意点

keyは一意である必要があります。ここでは各orderに一意なIDを割り振っていることが前提となっています。もし、IDが重複する可能性がある場合はユニークな値を生成して渡すようにする必要があります。

まとめ

keyを有効活用することで、useEffectの多用を避け、コードをより簡潔かつ効率的な状態にすることができます。「useEffectの多用でコードが複雑化している」と感じている人はぜひ活用を検討してみてください。

TrustHub テックブログ

Discussion