はじめに
Reactのテストで act() を使う場面は多いですが、本記事では act() がどのように動作するか、また async をつける意味を解説します。
問題
以下のようなuseEffectで非同期処理を行うカスタムフックのテストを考えます。
const useMyHook = () => {
const [data, setData] = useState(null);
useEffect(() => {
(async () => {
const result = await fetchData(); // 非同期処理
setData(result);
})();
}, []);
return { data };
};
act() でラップしてもテストが期待通りに動かないケースがあります。
// ❌ 非同期処理の完了を待てていない
act(() => {
renderHook(() => useMyHook());
});
expect(result.current.data).toBe("expected"); // まだ完了していない
なぜこうなるのでしょうか?
解決方法
act() の仕組み
act() は内部のキューに溜まった更新処理をまとめて実行(フラッシュ)し、DOMに反映させます。
Reactでは状態の更新やuseEffectの処理などをキューに入れます。
React公式ドキュメントには以下のように記載されています。
Any updates triggered within the actFn, are added to an internal act queue, which are then flushed together to process and apply any changes to the DOM.
つまり act() の役割は「キューに溜まった処理をすべて実行すること」です。
同期処理と非同期処理の違い
同期処理であればフラッシュ時にすべての処理が完了します。
act() がフラッシュする
└─ 同期処理 → そのまま完了まで実行される ✅
しかし非同期処理の場合、await で待つ処理は別のキューに積まれます。
別のキューに積まれるので非同期処理の完了を待ってくれなくなります。
act() がフラッシュする
└─ 非同期処理(await) → 別のキューに積まれる
↓
フラッシュ完了と判断してしまう ❌
↓
expectが実行される(非同期処理はまだ終わっていない)
async をつける意味
await act(async () => {}) とすることで、非同期処理の境界をまたいだ処理も完了まで実行してくれます。
こちらも公式ドキュメントに明記されています。
Since it is async, React will also run any code that crosses an async boundary, and flush any updates scheduled.
// ✅ 非同期処理の完了まで待つ
await act(async () => {
renderHook(() => useMyHook());
});
expect(result.current.data).toBe("expected"); // 完了している
まとめ
| act() | await act(async () => {}) | |
|---|---|---|
| 同期処理 | ✅ | ✅ |
| 非同期処理 | ❌ | ✅ |
非同期処理が含まれる場合は await act(async () => {}) を使うことで、処理の完了を保証できます。
おわりに
act() に async をつける意味は「非同期処理の境界をまたいだ処理も完了まで待つ」ことです。
非同期処理が含まれるかどうかを意識して使い分けることで、より信頼性の高いテストが書けます。