【実務・中級編】 act()ユーティリティによる副作用の同期実行 – React実践ガイド

Reactテストの「魔物」を飼い慣らす:act()と正しく向き合う技術

フロントエンドの現場でReactのテストを書いていると、一度は必ず遭遇するあの悪名高い警告がある。「An update to Component inside a test was not wrapped in act(…).」

この警告を見た瞬間、「またか……」とブラウザを閉じたくなったことはないだろうか。今日は、この警告の正体と、なぜ `act()` が必要なのか、そして現場でどう付き合うべきかについて、裏側のメカニズムまで深掘りして解説しよう。

—

1. なぜ `act()` が必要なのか?

Reactのレンダリングは、ブラウザの描画プロセスと同期するように設計されている。`setState` を呼んだからといって、次の行ですぐにDOMが書き換わるわけではない。Reactは「あ、状態が変わったな。じゃあ後でまとめて処理するか(バッチ処理)」と、内部的にタスクをキューイングして、適切なタイミングで一気にレンダリングを行う。

テスト環境はどうか?テストコードはNode.js上で走っており、ブラウザの「描画サイクル」は存在しない。つまり、「Reactがいつ処理を完了したか」をテストランナーが知る術がないのだ。

`act()` は、Reactに対して「今からここで起きる状態変化と副作用を、同期的にすべて処理してくれ!」と号令をかけるための関数だ。これを使わないと、Reactが処理を終える前にテストのアサーション(期待値確認)が走ってしまい、中途半端なDOMの状態を見てテストが落ちる、あるいは警告が出るというわけだ。

—

2. 現場で直面する「非同期」の罠

よくあるミスは、`useEffect` 内で非同期処理(APIフェッチなど)を行っているケースだ。

// 悪い例:レンダリング後に非同期で状態が更新されると、actで囲まれていないと怒られる
useEffect(() => {
fetchData().then(data => setData(data));
}, []);

この場合、`fetchData` が完了した後の `setData` が、Reactの管理外で実行されることになる。これをテストで通すには、単に `act` で囲むだけでは不十分だ。

—

3. 実践:クリーンで堅牢なテストパターン

中級者のエンジニアなら、`@testing-library/react` を使っているはずだ。実は、`render` や `fireEvent`、`userEvent` は、内部で既に `act()` を呼んでいる。だから、ユーザー操作をシミュレートするテストでは、ほとんどの場合 `act()` を意識する必要はない。

問題になるのは、「テスト開始直後の非同期処理」や「タイマー処理」だ。これらを綺麗に制御するパターンを伝授する。

import { render, screen, act } from ‘@testing-library/react’;
import MyComponent from ‘./MyComponent’;

// APIをモックした前提
test(‘APIフェッチ後のレンダリングが正しく行われること’, async () => {
// 1. レンダリングを開始する時点で、非同期の解決を待つためにactを使う
await act(async () => {
render();
});

// 2. この時点では、すでにレンダリングとuseEffect内の処理が完了している
const element = await screen.findByText(/データがロードされました/i);
expect(element).toBeInTheDocument();
});

Tips: `findBy` を活用する

実は、`await screen.findBy…` を使えば、内部で `waitFor` が動き、ポーリングしてDOMの出現を待ってくれる。無理に `act()` で囲い込むよりも、「何が画面に出たらテストを続行して良いか」というDOMの状態で待機する方が、テストの保守性は圧倒的に高くなる。

—

4. チーフアーキテクトからの助言

`act()` でテストを囲みまくるのは、正直なところ「泥縄式」の修正になりがちだ。もしコードを書いていて `act()` が頻発するなら、それはコンポーネントの責務が肥大化しているサインかもしれない。

以下の3点を見直してみてほしい:

1. 副作用をカスタムフックに逃がしているか?
副作用のロジックが複雑なら、コンポーネントの外に出してテストしやすくする。
2. 適切なMocksを使っているか?
タイマーやAPI通信は `jest.useFakeTimers()` や `msw` で完全に制御する。`act()` を使わなくても済むようにテスト環境を整えるのがプロの仕事だ。
3. `act()` は「どうしても必要な時」の最終手段にする
Reactの内部構造に強制介入する行為なので、乱用は禁物だ。

—

まとめ:Reactと「対話」する意識を持つ

`act()` は、Reactという巨大なエンジンを「一旦停止させて、中身を確認する」ための魔法の言葉だ。その仕組みを理解すれば、警告に怯える必要はなくなる。

テストを書くということは、単にバグを防ぐことではない。「自分のコードがどういう順序で動き、どういう結果を生むか」というReactとの対話なのだ。

今日の解説が、あなたのテストコードをよりクリーンで自信の持てるものに変える一助になれば幸いだ。迷ったら、またここに戻ってきてくれ。現場の最前線で、一緒に良いコードを書いていこう。

コメント

タイトルとURLをコピーしました