React测试触发状态更新时,何时需要使用act()?
为什么在假定时器触发状态更新时需要手动使用act()?
我用Vitest搭配@testing-library/react做组件测试,两个测试都能通过,但第二个测试里,当触发超时函数内的setBaz时,会弹出“需使用act()”的警告。点击Foo、Bar按钮触发的状态更新不会有这个问题,只有定时器里的状态更新会触发警告。
组件代码
export function Foo() { const [foo, setFoo] = useState(''); const [bar, setBar] = useState(''); const [baz, setBaz] = useState(''); useEffect(() => { if (bar) setTimeout(() => setBaz('baz'), 5000); }, [bar]); return ( <> <button onClick={() => setFoo('foo')}>Foo: {foo}</button> <button onClick={() => setBar('bar')}>Bar: {bar}</button> <div>Baz: {baz}</div> </> ); }
测试代码
test('should update foo', async () => { const user = userEvent.setup(); render(<Foo />); const fooBtn = screen.getByText(/Foo:/); await user.click(fooBtn); expect(fooBtn.innerHTML).toEqual('Foo: foo'); }); /* 警告:An update to Foo inside a test was not wrapped in act(...). When testing, code that causes React state updates should be wrapped into act(...): */ test('should update baz after timer', async () => { vi.useFakeTimers({ shouldAdvanceTime: true }); const user = userEvent.setup(); render(<Foo />); const barBtn = screen.getByText(/Bar:/); await user.click(barBtn); vi.advanceTimersByTime(5000); // 这里会触发警告 await waitFor(() => { const bazEl = screen.getByText(/Baz:/); expect(bazEl.innerHTML).toEqual('Baz: baz'); }); });
把vi.advanceTimersByTime(5000)包裹在act(() => vi.advanceTimersByTime(5000))里就能消除警告,下面说清这类场景下什么时候需要用act():
先搞懂act()的作用
React的act()本质是用来同步组件的状态更新和渲染——它会等待所有包裹在内的代码引发的组件渲染、状态更新完成后,再执行后续逻辑,让测试的行为和真实浏览器里的用户操作保持一致。
为什么有的场景不用手动写act()?
@testing-library/react提供的大部分工具(比如render、userEvent.click、waitFor)已经内部自动封装了act()。比如第一个测试里的await user.click(fooBtn),点击触发的setFoo状态更新,已经被userEvent的click方法自动包裹在act里了,所以不会出警告。
什么时候必须手动用act()?
当你执行的操作不在Testing Library工具的覆盖范围内,但会直接触发组件的状态更新时,就需要手动用act()包裹:
- 直接操作假定时器(比如
vi.advanceTimersByTime、jest.runAllTimers),因为推进定时器会触发内部的回调(比如这里的setBaz),进而引发状态更新 - 手动调用组件内部的状态更新函数(比如直接调用
setFoo) - 手动触发异步回调(比如直接调用setTimeout里的回调函数)
简单说:只要你的测试代码里有自己写的、会触发组件状态更新,但没被Testing Library工具自动处理的逻辑,就需要用act()把它包起来。
修正后的测试代码
test('should update baz after timer', async () => { vi.useFakeTimers({ shouldAdvanceTime: true }); const user = userEvent.setup(); render(<Foo />); const barBtn = screen.getByText(/Bar:/); await user.click(barBtn); // 用act包裹推进定时器的操作 act(() => { vi.advanceTimersByTime(5000); }); await waitFor(() => { const bazEl = screen.getByText(/Baz:/); expect(bazEl.innerHTML).toEqual('Baz: baz'); }); });
内容的提问来源于stack exchange,提问作者Joe
相关产品推荐
相关产品推荐

