React触发act警告的具体条件是什么?
问题场景:触发警告的示例
组件代码
import React, { useState } from "react"; export function Example1(props: { onClick: () => Promise<void>; }) { const [value, setValue] = useState(0); return ( <div> <button onClick={async () => { await props.onClick(); setValue(1); }}>Click me</button> <h1>Hello, World!</h1> <p>{value}</p> </div> ); }
测试代码
describe(Example1, () => { it("renders", async () => { const mockFn = jest.fn(async () => {}); render(<Example1 onClick={mockFn}/>); expect(screen.getByText("Hello, World!")).toBeInTheDocument(); fireEvent.click(screen.getByRole("button", {name: "Click me"})); expect(mockFn).toHaveBeenCalled(); }) });
触发的警告
An update to Example1 inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
常规认知的误区
Kent C Dodds曾解释过act警告的意义:
So the act warning from React is there to tell us that something happened to our component when we weren't expecting anything to happen. So you're supposed to wrap every interaction you make with your component in act to let React know that we expect our component to perform some updates and when you don't do that and there are updates, React will warn us that unexpected updates happened. This helps us avoid bugs like the one described above.
... BUT, React Testing Library has async utilities that are wrapped in act automatically!
常规解决方法是添加等待组件变化的断言,但这无法解释为什么部分异步场景(比如组件内部await异步函数、onClick里用setTimeout)不会触发警告——这说明异步不是触发act警告的唯一条件。
核心触发因素
act警告触发的核心是:状态更新发生在React无法追踪的异步上下文里,且这个更新发生在测试的同步执行阶段内,需同时满足两个必要条件:
1. 状态更新不在act的预期范围内
React Testing Library的fireEvent等交互API本身被act包裹,会告知React“接下来的交互可能触发组件更新”。但如果交互后存在外部传入的异步操作(比如示例中props里的onClick回调),React无法追踪这个异步操作的生命周期——该Promise不属于组件内部逻辑,React不知道它何时resolve,也就无法将后续状态更新纳入act上下文。
当外部Promise resolve后执行setValue时,React会判定这是“意外的、未被预期的更新”,进而抛出警告。
2. 状态更新发生在测试的同步执行阶段
- 如果异步操作是组件内部定义的(比如模块内的async函数),React能通过组件上下文关联该异步操作与之前的交互,React Testing Library会自动等待这类异步操作完成,确保状态更新被包裹在act中,不会触发警告。
- 对于
setTimeout这类宏任务,状态更新会延迟到测试同步代码(包括fireEvent和所有expect断言)全部执行完毕后才触发——此时测试已结束,React不会在测试运行期间检测到该更新,因此也不会抛出警告。
解决方法的本质
要消除警告,核心是让React知晓“我们预期该异步操作完成后会有状态更新”,常见做法:
- 用
waitFor包裹等待状态变化的断言:await waitFor(() => { expect(screen.getByText("1")).toBeInTheDocument(); }); - 直接await外部异步操作的完成(若能获取引用):
await mockFn();
这样就能让React将后续状态更新纳入act的预期范围内,避免警告。
内容的提问来源于stack exchange,提问作者dwjohnston

