はじめに
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
の更新は非同期で行われます。そのため:
-
setItems
を呼び出しても、その直後の同じスコープ内ではitems
の値は更新されていません -
items[items.length - 1]
で取得されるのは、新しく追加された項目ではなく、一つ前の古い項目のIDになってしまいます - 結果として、意図した要素にフォーカスが当たらない
正しい実装方法
この問題は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>
);
};
この実装のポイント:
- アイテムの追加処理とフォーカス制御を分離
-
useEffect
でitems
の変更を監視 - 実際に値が更新された後にフォーカス制御を実行
バグの発見が難しい理由
このようなバグが見つけにくい理由として:
- 開発環境では処理が早すぎて問題が顕在化しにくい
- 別の実装(useEffectなど)で意図した動作が実現されている場合がある
- コードレビューでも見落としやすい
まとめ
Reactでstateを扱う際の重要なポイント:
-
useState
の更新は非同期で行われる - 更新直後の同じスコープ内では新しい値は反映されていない
- 更新後の値を使用したい場合は
useEffect
を使用する - 状態更新とその後の副作用は分離して管理する
これらの原則を理解していれば、より堅牢なReactアプリケーションを作ることができます。