この記事の途中に、以下の記事の引用を含んでいます。
Everything you need to know about act() in React tests
そもそもact()って何?――Reactテストに欠かせない謎多き存在
Reactでのテストを書き始めると、必ず遭遇するのが「act()」という関数です。
「これって何のためにあるの?いつ使うの?」と、多くの人が一度は疑問に思います。
この記事は、Reactのテストをする上で頻出するにもかかわらず誤解されがちな「act()」の役割と適切な使い方を、具体的な事例と共に徹底的に解説していることが特徴です。
公式ドキュメントや多くの記事でもact()は説明されていますが、「なぜ必要か」「どうして警告が出るのか」「React Testing LibraryとReactからのact()の違い」「いつ不要なのか」など、痒いところに手が届く話題まで網羅されています。
何が主張されているのか?――記事の要点と実際の警告の例
まず、記事はこう主張しています。
“In your tests, functionality that updates internal state of a rendered React component should be wrapped in act() so we can be sure that all state changes and side effects have been fully processed by React, before the rest of your test (i.e. assertions) continues.”
(テスト内で状態更新や副作用が発生する処理はact()でラップすべき。これによりReactの状態変化が完全に反映されてから、アサーションに進むことが保証される、と述べられています。)
また、おなじみの警告メッセージも事例として紹介されています。
“An update to %s inside a test was not wrapped in act(…). When testing, code that causes React state updates should be wrapped into act(…): act(() => { / fire events that update state / }); / assert on the output /”
(つまり、「stateを更新する処理はact()で囲んでね!」というReactの警告が出るというわけです。)
なぜact()が必須なのか?――リアルなUIの再現性とテスト精度を担保
act()は“テスト環境のタイムトラベルマシン”
ここからがact()の本質です。
Reactはstateや副作用の更新を“非同期的”かつ“バッチ的”に扱うため、例えばsetTimeoutや非同期APIの結果でstateが変わると、想定通りに反映されないことがあります。
そんな状況でact()がなぜ必要か?
それは、“act()の内部で実行されたstate・副作用の変化をすべて終了まで待つ”ことをテストランナー側に保証させるからです。
実際、テストに「act()を忘れる」とリアルの挙動と食い違う「幽霊バグ」や「古いstateでのアサーション」に繋がりかねません。
React Testing LibraryとReact本体のact()の違い
“act()どれをimportするか問題”もよく出てきますね。結論はこうです。
“Always import from @testing-library/react :
import { act } from '@testing-library/react';This…just wraps around the actual act() implementation from react, but with these changes: ensures some environment configuration is in place…
つまり、バージョン差異や挙動の問題を避けるためにも、基本は@testing-library/reactからimportしましょう(現行ではレガシーな事情もほぼ払拭されていますが一貫性が重要、という指摘がされています)。
act()の使い所:UIイベントからhookテスト、非同期挙動まで
- manualなDOM操作(button.clickなど)
- フェイクタイマー利用時(setTimeout, setIntervalのテスト)
- カスタムhookの状態遷移テスト
- fireEvent・直接dispatchするイベント
上記シーンでは、必ずact()でラップし、状態変化の完全な反映を確実にしましょう。
実際のコード事例で見るact()の真価――「これでテスト落とさない!」
例1:setTimeoutによる状態更新
jsx
const AutoCounter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
setTimeout(() => {
setCount(c => c + 1);
}, 10);
}, []);
return <h1>Count: {count}</h1>;
};
act()を付け忘れたケース:
js
test('auto-increments after 10ms', () => {
render(<AutoCounter />);
const heading = screen.getByRole('heading');
expect(heading).toHaveTextContent('Count: 0');
vi.advanceTimersByTime(10);
expect(heading).toHaveTextContent('Count: 1'); // ← 失敗するかも!
});
act()でラップすればOK:
js
test('auto-increments after 10ms', async () => {
render(<AutoCounter />);
const heading = screen.getByRole('heading');
expect(heading).toHaveTextContent('Count: 0');
await act(async () => {
vi.advanceTimersByTime(10);
});
expect(heading).toHaveTextContent('Count: 1'); // ← 成功!
});
例2:hookの状態変化検証
jsx
const useCounter = () => {
const [value, setValue] = useState(0);
const increment = () => setValue(v => v + 1);
return { increment, value };
};
act()なし:
js
test('can increment the counter', () => {
const { result } = renderHook(useCounter);
expect(result.current.value).toBe(0);
result.current.increment();
expect(result.current.value).toBe(1); // ← 実は失敗…
});
act()あり:
js
test('can increment the counter', async () => {
const { result } = renderHook(useCounter);
expect(result.current.value).toBe(0);
await act(async () => {
result.current.increment();
});
expect(result.current.value).toBe(1); // ← 正しく反映!
});
この記事で紹介されているように、「UI的に何も変化しない」「テストだけ落ちる」原因が、act()の有無だったということが驚くほど多いです。
本当に全部act()で囲むべき?——「過剰ラッピング」の落とし穴
意外なことに、act()は「すべての場合」で必要なのでは?という誤解も生まれがちです。
しかし記事ではこう指摘があります。
“Do not wrap React Testing Library functions in act().
(They do it internally anyway.)”
つまり、userEventやfindBy...、waitForといったRTLの関数は内部でact()を自動的に使っているので、自分ではラップしなくてOKです。
むしろ“act()の過剰運用”はパフォーマンスやテストの実装コストを無駄にすることも…。
じゃあ、どのタイミングで生act()が本当に必要なのか?
userEvent/findBy...で用が足りるなら、そちらを優先。- 明確なstate変更トリガが直にテストから発火される場合(例:
button.click()を直接実行、カスタムhookのコールバック呼び出し)、忘れずact()でラップ。 - テスト出力に「act()でラップされてません!」という警告が出てきたら、そこが書き直しポイント。
こうした線引きは一見ややこしいですが、現実のプロダクトコードやテストの保守性を考えると極めて重要です。
act()の「async版」は使うべき?――技術動向と現時点でのベストプラクティス
近年Reactチームの公式見解もあり、await act(async () => { ... })のasync版利用がデファクトになりつつあります。
こう説明されています。
“You should always use async when:
The code inside act() contains promises, async/await, or timers
If you are not sure if you need to – because async is always safe”
つまり、内部処理が同期的かどうかに関わらず、「迷ったらasync」!
特に最近(2025〜)のエコシステムでは、非async版は非推奨・将来的に廃止の動きもあります。
act()不要論への注意点――「テストが落ちてからでは遅い」
一部コミュニティで「act()使わなくてもfindByTextで済むし…」という話もあります。
確かにawait screen.findByText(...)やwaitFor()で済めばベターです。
ですが、
– ユーザイベントや副作用の発生タイミング、タイマーの制御など「本来は反映されるはず」のstate更新が正しくテスト上に現れない場合
– 何をやっても謎の警告が消えない場合
– 複雑なカスタムhookやコンポーネントの設計で、テストロジックの単純化より確定的な副作用の完了が重要な場合
上記のようなシーンでは今なお「検証操作の明示的な境界」を作る目的でact()は欠かせません。
テスト書きの現場で役立つTIPS:act()の微妙な罠とデバッグの極意
act()“警告”が出た時のプロセス
- どのテストで発生しているかisolatedに絞る(test.only等を多用)
- 1行ずつreturnを挟み“犯人”操作を吐き出す
- 対象の状態変化部分がact()で囲まれていなければ「そこが原因」
- await漏れ・ダブルact()ラップ・サードパーティcomponent周り・useEffectのcleanup漏れも疑う
- render後やtest終了時に状態変化が走る場合には、test終端に
await act(() => {})でflushする
状態変化を伴うテストには、極力「短く・小さく」act()を使う
1行・1イベント・1function単位でラップするのが基本です。
また、何でもかんでもact()で抱きかかえるのはやめましょう。
最後に――テストと現実世界を繋ぐ“act()”の真価
act()とは――「テストが本当にリアルと同じ挙動をしているか?」を確実に担保するための“synchronization hook”です。
変化がある部分だけ、必要なだけ、しかも短く使う。
React Testing Libraryの“哲学”である「できるだけユーザ視点で・現実的に」コードを動かし、バグ混入リスクやfalse positive・negativeを極力減らすための必須知識となります。
「テスト、書いてるのになぜか現実と違う…」
「警告が消えない…」
――そのモヤモヤの正体こそ、act()の理解と使い方にあるケースがほとんどです。
これからReactに取り組む方も、すでに警告に悩まされている方も、
「必要最小限、正しい場所に」act()を配置すること──
これが堅牢なテストとバグの少ないプロダクト開発へのパスポートです。
categories:[technology]


コメント