1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

React useState更新直後の値参照が引き起こす意図しない挙動とその解決策

Posted at

はじめに

Reactで開発をしていると、useStateでstateを更新した直後にその値を使いたくなることがありますよね。しかし、これは思わぬバグを引き起こす可能性があります。この記事では具体例を通じて、よくある落とし穴とその解決方法を解説します。

問題のあるコード例

以下のような商品リストにアイテムを追加する機能を考えてみましょう:

const ShoppingList = () => {
  const [items, setItems] = useState<string[]>([]);
  const itemRefs = useRef<{ [key: string]: HTMLDivElement | null }>({});

  const addItem = useCallback(() => {
    const newItemId = generateId(); // ユニークなIDを生成
    setItems(prev => [...prev, newItemId]);
    
    // 追加したアイテムにフォーカスを当てたい
    const lastItemId = items[items.length - 1];
    const element = itemRefs.current[lastItemId];
    if (element) {
      const input = element.querySelector('input');
      input?.focus();
    }
  }, [items]);

  return (
    <div>
      {items.map(id => (
        <div key={id} ref={el => itemRefs.current[id] = el}>
          <input type="text" placeholder="商品名を入力" />
        </div>
      ))}
      <button onClick={addItem}>商品を追加</button>
    </div>
  );
};

一見問題なさそうに見えますが、このコードには重大な問題があります。

何が問題なのか?

問題はsetItemsによる状態更新が即座には反映されないということです。

ReactのuseStateの更新は非同期で行われます。そのため:

  1. setItemsを呼び出しても、その直後の同じスコープ内ではitemsの値は更新されていません
  2. items[items.length - 1]で取得されるのは、新しく追加された項目ではなく、一つ前の古い項目のIDになってしまいます
  3. 結果として、意図した要素にフォーカスが当たらない

正しい実装方法

この問題はuseEffectを使用することで適切に解決できます:

const ShoppingList = () => {
  const [items, setItems] = useState<string[]>([]);
  const itemRefs = useRef<{ [key: string]: HTMLDivElement | null }>({});
  const previousLength = useRef(items.length);

  const addItem = useCallback(() => {
    const newItemId = generateId();
    setItems(prev => [...prev, newItemId]);
  }, []);

  // itemsの変更を検知して適切なタイミングでフォーカスを制御
  useEffect(() => {
    if (items.length > previousLength.current) {
      const lastItemId = items[items.length - 1];
      const element = itemRefs.current[lastItemId];
      if (element) {
        const input = element.querySelector('input');
        input?.focus();
      }
    }
    previousLength.current = items.length;
  }, [items]);

  return (
    <div>
      {items.map(id => (
        <div key={id} ref={el => itemRefs.current[id] = el}>
          <input type="text" placeholder="商品名を入力" />
        </div>
      ))}
      <button onClick={addItem}>商品を追加</button>
    </div>
  );
};

この実装のポイント:

  1. アイテムの追加処理とフォーカス制御を分離
  2. useEffectitemsの変更を監視
  3. 実際に値が更新された後にフォーカス制御を実行

バグの発見が難しい理由

このようなバグが見つけにくい理由として:

  1. 開発環境では処理が早すぎて問題が顕在化しにくい
  2. 別の実装(useEffectなど)で意図した動作が実現されている場合がある
  3. コードレビューでも見落としやすい

まとめ

Reactでstateを扱う際の重要なポイント:

  1. useStateの更新は非同期で行われる
  2. 更新直後の同じスコープ内では新しい値は反映されていない
  3. 更新後の値を使用したい場合はuseEffectを使用する
  4. 状態更新とその後の副作用は分離して管理する

これらの原則を理解していれば、より堅牢なReactアプリケーションを作ることができます。

参考リンク

1
0
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?