惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

G
Google Developers Blog
S
Schneier on Security
The Hacker News
The Hacker News
P
Proofpoint News Feed
Spread Privacy
Spread Privacy
L
LINUX DO - 热门话题
L
Lohrmann on Cybersecurity
I
Intezer
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Schneier on Security
Schneier on Security
Security Latest
Security Latest
AWS News Blog
AWS News Blog
B
Blog RSS Feed
Microsoft Security Blog
Microsoft Security Blog
有赞技术团队
有赞技术团队
博客园 - 叶小钗
The Last Watchdog
The Last Watchdog
O
OpenAI News
月光博客
月光博客
Hacker News: Ask HN
Hacker News: Ask HN
阮一峰的网络日志
阮一峰的网络日志
S
Security @ Cisco Blogs
Google Online Security Blog
Google Online Security Blog
云风的 BLOG
云风的 BLOG
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Latest news
Latest news
P
Palo Alto Networks Blog
Last Week in AI
Last Week in AI
M
MIT News - Artificial intelligence
Google DeepMind News
Google DeepMind News
P
Proofpoint News Feed
C
CERT Recently Published Vulnerability Notes
Apple Machine Learning Research
Apple Machine Learning Research
U
Unit 42
PCI Perspectives
PCI Perspectives
博客园 - 聂微东
SecWiki News
SecWiki News
宝玉的分享
宝玉的分享
Forbes - Security
Forbes - Security
H
Heimdal Security Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Hugging Face - Blog
Hugging Face - Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
T
Troy Hunt's Blog
博客园 - 三生石上(FineUI控件)
Application and Cybersecurity Blog
Application and Cybersecurity Blog
罗磊的独立博客
WordPress大学
WordPress大学
D
Darknet – Hacking Tools, Hacker News & Cyber Security

Kent C. Dodds Blog

Implementing Hybrid Semantic + Lexical Search Simplifying Containers with Cloudflare Sandboxes Migrating to Workspaces and Nx Offloading FFmpeg with Cloudflare Building Semantic Search on my Content Helping YOU ask ME questions with AI How I used Cursor to Migrate Frameworks The Dow's Start on the Covenant Path 2025 in Review The next chapter: EpicAI.pro AI is taking your job How I increased my visibility Launching Epic Web 2023 in Review Stop Being a Junior RSC with Dan Abramov and Joe Savona Live Stream Fixing a Memory Leak in a Production Node.js App 2022 in Review My Car Accident I Migrated from a Postgres Cluster to Distributed SQLite with LiteFS I'm building EpicWeb.dev A review of my time at Remix Remix: The Yang to React's Yin How I help you build better websites Why I Love Remix The State Initializer Pattern How to React ⚛️ Get a catch block error message with TypeScript Building an awesome image loading experience How Remix makes CSS clashes predictable Introducing the new kentcdodds.com How I built a modern website in 2021 How to use React Context effectively Static vs Unit vs Integration vs E2E Testing for Frontend Apps The Testing Trophy and Testing Classifications Array reduce vs chaining vs for loop Don't Solve Problems, Eliminate Them Super Simple Start to Remix Super Simple Start to ESModules in Node.js JavaScript Pass By Value Function Parameters How to write a Constrained Identity Function (CIF) in TypeScript How to optimize your context value How to write a React Component in TypeScript TypeScript Function Syntaxes Listify a JavaScript Array Build vs Buy: Component Libraries edition Using fetch with TypeScript Wrapping React.useState with TypeScript Define function overload types with TypeScript 2020 in Review Business and Engineering alignment Hi, thanks for reaching out to me 👋 useEffect vs useLayoutEffect Super simple start to Firebase functions Super simple start to Netlify functions Super Simple Start to css variables Favor Progress Over Pride in Open Source Testing Implementation Details How getting into Open Source has been awesome for me useState lazy initialization and function updates Use ternaries rather than && in JSX Application State Management with React Use react-error-boundary to handle errors in React JavaScript to Know for React How I structure Express apps What open source project should I contribute to? When I follow TDD AHA Programming 💡 How I Record Educational Videos Should I write a test or fix a bug? Stop mocking fetch Intentional Career Building Improve test error messages of your abstractions Tracing user interactions with React Eliminate an entire category of bugs with a few simple tools Common mistakes with React Testing Library Super Simple Start to React Stop using client-side route redirects The State Reducer Pattern with React Hooks Function forms Replace axios with a simple custom fetch wrapper How to test custom React hooks React Production Performance Monitoring Should I useState or useReducer? Stop using isLoading booleans Make Your Test Fail Make your own DevTools An Argument for Automation Super Simple Start to ESModules in the Browser Implementing a simple state machine library in JavaScript 2010s Decade in Review Why users care about how you write code Why I avoid nesting closures Don't call a React function component Why your team needs TestingJavaScript.com Inversion of Control Understanding React's key prop How to Enable React Concurrent Mode How to add testing to an existing project Profile a React App for Performance
Fix the "not wrapped in act(...)" warning
2020-02-03 · via Kent C. Dodds Blog

Watch "Fix the "not wrapped in act(...)" warning" on egghead.io

Watch "Fix the "not wrapped in act(...)" warning with Jest fake timers" on egghead.io

Watch "Fix the "not wrapped in act(...)" warning when testing Custom Hooks" on egghead.io

Watch "Fix the "not wrapped in act(...)" warning when testing the useImperativeHandle Hook" on egghead.io

Imagine you have a component like this:

Here's the code for that (if you were to do it with class components, don't worry a function version is coming later in the post):

class UsernameFormClass extends React.Component {
	state = { status: 'idle', error: null }
	handleSubmit = async (event) => {
		event.preventDefault()
		const newUsername = event.target.elements.username.value
		this.setState({ status: 'pending' })
		try {
			await this.props.updateUsername(newUsername)
			this.setState({ status: 'fulfilled' })
		} catch (e) {
			this.setState({ status: 'rejected', error: e })
		}
	}
	render() {
		const { error, status } = this.state

		return (
			<form onSubmit={this.handleSubmit}>
				<label htmlFor="username">Username</label>
				<input id="username" />
				<button type="submit">Submit</button>
				<span>{status === 'pending' ? 'Saving...' : null}</span>
				<span>{status === 'rejected' ? error.message : null}</span>
			</form>
		)
	}
}

So, let's imagine we want to write a test for this. Here's what you might write to verify that the component is working properly:

import * as React from 'react'
import user from '@testing-library/user-event'
import { render, screen } from '@testing-library/react'

test('calls updateUsername with the new username', async () => {
	const handleUpdateUsername = jest.fn()
	const fakeUsername = 'sonicthehedgehog'

	render(<UsernameForm updateUsername={handleUpdateUsername} />)

	const usernameInput = screen.getByLabelText(/username/i)
	user.type(usernameInput, fakeUsername)
	user.click(screen.getByText(/submit/i))

	expect(handleUpdateUsername).toHaveBeenCalledWith(fakeUsername)
})

Great! So now, if we make some sort of typo and not call the updateUsername function or we forget to call it with the new username then our test will fail and it provides us value.

But, what if we rewrite this to a function component with hooks? Let's try that:

function UsernameForm({ updateUsername }) {
	const [{ status, error }, setState] = React.useState({
		status: 'idle',
		error: null,
	})

	async function handleSubmit(event) {
		event.preventDefault()
		const newUsername = event.target.elements.username.value
		setState({ status: 'pending' })
		try {
			await updateUsername(newUsername)
			setState({ status: 'fulfilled' })
		} catch (e) {
			setState({ status: 'rejected', error: e })
		}
	}

	return (
		<form onSubmit={handleSubmit}>
			<label htmlFor="username">Username</label>
			<input id="username" />
			<button type="submit">Submit</button>
			<span>{status === 'pending' ? 'Saving...' : null}</span>
			<span>{status === 'rejected' ? error.message : null}</span>
		</form>
	)
}

And the cool thing about React Testing Library is because it's free of implementation details we can run those exact same tests with this refactored version of our component.

The dreaded act(...) warning

Unfortunately, if we try that, we'll get this really annoying warning in the console that looks like this:

console.error node_modules/react-dom/cjs/react-dom.development.js:530
  Warning: An update to UsernameForm 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 */

  This ensures that you're testing the behavior the user would see in the browser. Learn more at https://fb.me/react-wrap-tests-with-act
      in UsernameForm

What the!? So what's going on here? Well, you might not have noticed, but our test didn't test our component's happy-path fully and left one important aspect vulnerable to regressions. Let's go back to the class version and comment out an important line of code:

class UsernameFormClass extends React.Component {
	state = { status: 'idle', error: null }
	handleSubmit = async (event) => {
		event.preventDefault()
		const newUsername = event.target.elements.username.value
		this.setState({ status: 'pending' })
		try {
			await this.props.updateUsername(newUsername)
			// this.setState({status: 'fulfilled'})
		} catch (e) {
			this.setState({ status: 'rejected', error: e })
		}
	}
	render() {
		const { error, status } = this.state

		return (
			<form onSubmit={this.handleSubmit}>
				<label htmlFor="username">Username</label>
				<input id="username" />
				<button type="submit">Submit</button>
				<span>{status === 'pending' ? 'Saving...' : null}</span>
				<span>{status === 'rejected' ? error.message : null}</span>
			</form>
		)
	}
}

Guess what! Our tests still pass! But, the Saving... loading indicator next to our submit button never goes away:

Oh no! It sure would've been nice if we'd had some warning that we weren't testing all that our component was doing so we could've written a good test from the start. Wait... Is that what act was yelling at us about? YES IT WAS!

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.

Luckily for you and me, React automatically handles this for any of your code that's running within the React callstack (like click events where React calls into your event handler code which updates the component), but it cannot handle this for any code running outside of it's own callstack (like asynchronous code that runs as a result of a resolved promise you are managing or if you're using jest fake timers). With those kinds of situations you typically need to wrap that in act(...) or async act(...) yourself. BUT, React Testing Library has async utilities that are wrapped in act automatically!

How to fix the act(...) warning

So, let's first thank the React team for letting us know that we didn't test everything that's happening in our component (thanks React team! 🙏) and then let's add an assertion to our test to cover the use case we forgot the first time around:

test('calls updateUsername with the new username', async () => {
	const handleUpdateUsername = jest.fn()
	const fakeUsername = 'sonicthehedgehog'

	render(<UsernameForm updateUsername={handleUpdateUsername} />)

	const usernameInput = screen.getByLabelText(/username/i)
	user.type(usernameInput, fakeUsername)
	user.click(screen.getByText(/submit/i))

	expect(handleUpdateUsername).toHaveBeenCalledWith(fakeUsername)
	await waitForElementToBeRemoved(() => screen.queryByText(/saving/i))
})

Great, so now we're waiting for the saving text to be removed and that ensures that the saving text appears in the first place and is removed when saving is complete.

You'll notice that this test passes whether we're using class components or function components (+1 point for avoiding Testing Implementation Details), but without that extra line we only get the warning with function components not class components. The reason for this is because the React team couldn't reasonably add this warning for class components without creating a TON of new warnings for people and their existing tests. So while the warning would probably help people find and fix bugs like this, the decision was made to only apply it to hooks so people would get the warning as they develop and test new components (+1 point for using function components over class components).

You can read more about React's act utility here, but again, you shouldn't have to use it very often. It's built-into React Testing Library. There are very few times you should have to use it directly if you're using React Testing Library's async utilities.

If you're still experiencing the act warning, then the most likely reason is something is happening after your test completes for which you should be waiting (like in our earlier examples).

An Alternative: waiting for the mocked promise

There's one alternative I want to show you that isn't quite as good for this use case, but could be useful, especially if there's no visual indication of the async task completing.

Because our code waits for the updateUsername promise to resolve before continuing, we could return a promise from our fake version and use an async act to await that promise resolution:

test('calls updateUsername with the new username', async () => {
	const promise = Promise.resolve() // You can also resolve with a mocked return value if necessary
	const handleUpdateUsername = jest.fn(() => promise)
	const fakeUsername = 'sonicthehedgehog'

	render(<UsernameForm updateUsername={handleUpdateUsername} />)

	const usernameInput = screen.getByLabelText(/username/i)
	user.type(usernameInput, fakeUsername)
	user.click(screen.getByText(/submit/i))

	expect(handleUpdateUsername).toHaveBeenCalledWith(fakeUsername)
	// we await the promise instead of returning directly, because act expects a "void" result
	await act(async () => {
		await promise
	})
})

Note that we're manually calling act here and you can get that from react-dom/test-utils or React Testing Library re-exports it so you can grab it from the import you already have. You're welcome.

This isn't preferable because it's still not going to catch the bug we demonstrated earlier by commenting out that setState call, but it does make the warning go away properly. I'd just recommend making additional assertions about the way things look after the async action has completed.

Other use cases for manually calling act(...)

If you're using all the React Testing Library async utilities and are waiting for your component to settle before finishing your test and you're still getting act warnings, then you may need to use act manually. Here are a few examples:

1. When using jest.useFakeTimers()

Let's say you have a component that's checking against an API on an interval:

function OrderStatus({ orderId }) {
	const [{ status, data, error }, setState] = React.useReducer(
		(s, a) => ({ ...s, ...a }),
		{ status: 'idle', data: null, error: null },
	)

	React.useEffect(() => {
		let current = true
		function tick() {
			setState({ status: 'pending' })
			checkStatus(orderId).then(
				(d) => {
					if (current) setState({ status: 'fulfilled', data: d })
				},
				(e) => {
					if (current) setState({ status: 'rejected', error: e })
				},
			)
		}
		const id = setInterval(tick, 1000)
		return () => {
			current = false
			clearInterval(id)
		}
	}, [orderId])

	return (
		<div>
			Order Status:{' '}
			<span>
				{status === 'idle' || status === 'pending'
					? '...'
					: status === 'error'
						? error.message
						: status === 'fulfilled'
							? data.orderStatus
							: null}
			</span>
		</div>
	)
}

So we've handled all the edge cases and cleaned up after ourselves nicely, but when we write our test for it, we're going to get the act warning again:

import * as React from 'react'
import { render, screen } from '@testing-library/react'
import { checkStatus } from '../api'

jest.mock('../api')

beforeAll(() => {
	// we're using fake timers because we don't want to
	// wait a full second for this test to run.
	jest.useFakeTimers()
})

afterAll(() => {
	jest.useRealTimers()
})

test('polling backend on an interval', async () => {
	const orderId = 'abc123'
	const orderStatus = 'Order Received'
	checkStatus.mockResolvedValue({ orderStatus })

	render(<OrderStatus orderId={orderId} />)

	expect(screen.getByText(/\.\.\./i)).toBeInTheDocument()
	expect(checkStatus).toHaveBeenCalledTimes(0)

	// advance the timers by a second to kick off the first request
	jest.advanceTimersByTime(1000)

	expect(await screen.findByText(orderStatus)).toBeInTheDocument()

	expect(checkStatus).toHaveBeenCalledWith(orderId)
	expect(checkStatus).toHaveBeenCalledTimes(1)
})

The act warning here is happening because of this line:

// ...
let current = true
function tick() {
  setState({status: 'pending'})
  checkStatus(orderId).then(
    d => {
// ...

The tick function is happening outside of React's callstack, so it's unsure whether this interaction with the component is properly tested. React Testing Library does not have a utility for jest fake timers and so we need to wrap the timer advancement in act ourselves, like this:

import * as React from 'react'
import { render, screen, act } from '@testing-library/react'
import { checkStatus } from '../api'

jest.mock('../api')

test('polling backend on an interval', async () => {
	const orderId = 'abc123'
	const orderStatus = 'Order Received'
	checkStatus.mockResolvedValue({ orderStatus })

	render(<OrderStatus orderId={orderId} />)

	expect(screen.getByText(/\.\.\./i)).toBeInTheDocument()
	expect(checkStatus).toHaveBeenCalledTimes(0)

	// advance the timers by a second to kick off the first request
	act(() => jest.advanceTimersByTime(1000))

	expect(await screen.findByText(orderStatus)).toBeInTheDocument()

	expect(checkStatus).toHaveBeenCalledWith(orderId)
	expect(checkStatus).toHaveBeenCalledTimes(1)
})

And now the React act(...) warning goes away!

Notice, that we can pull React DOM's act testing utility directly from @testing-library/react because it's simply re-exported by @testing-library/react (cool right!?).

2. When testing custom hooks

You'll get an act(...) warning with custom hooks when you call functions that are returned from your custom hook which result in state updates. Here's a simple example of that:

import * as React from 'react'

function useCount() {
	const [count, setCount] = React.useState(0)
	const increment = () => setCount((c) => c + 1)
	const decrement = () => setCount((c) => c - 1)
	return { count, increment, decrement }
}

export default useCount

Here, we'll use @testing-library/react to validate our hook works:

import * as React from 'react'
import { renderHook } from '@testing-library/react'
import useCount from '../use-count'

test('increment and decrement updates the count', () => {
	const { result } = renderHook(() => useCount())
	expect(result.current.count).toBe(0)

	result.current.increment()
	expect(result.current.count).toBe(1)

	result.current.decrement()
	expect(result.current.count).toBe(0)
})

When we call the increment and decrement functions from our hook, that triggers a state update and because we're not in the React callstack we're going to get an act(...) warning. So, let's wrap that in act(...)!

import * as React from 'react'
import { renderHook, act } from '@testing-library/react'
import useCount from '../use-count'

test('increment and decrement updates the count', () => {
	const { result } = renderHook(() => useCount())
	expect(result.current.count).toBe(0)

	act(() => result.current.increment())
	expect(result.current.count).toBe(1)

	act(() => result.current.decrement())
	expect(result.current.count).toBe(0)
})

You'll notice that we're pulling act from @testing-library/react and that's because it simply re-exports the act function from react-test-renderer so you don't need to add an additional export. Nice right!?

3. When using useImperativeHandle

You may not have even heard of this hook, and if you have you may not have run into this problem. That's because you only run into this if you're calling methods directly on a component which do internal state updates and you're outside of React's callstack. Let's read through an example of act warnings popping up when testing components that use this hook.

So here's a pretty contrived example, but you should be double-thinking your decisions any time you use this hook anyway:

function ImperativeCounter(props, ref) {
	const [count, setCount] = React.useState(0)
	React.useImperativeHandle(ref, () => ({
		increment: () => setCount((c) => c + 1),
		decrement: () => setCount((c) => c - 1),
	}))
	return <div>The count is: {count}</div>
}
ImperativeCounter = React.forwardRef(ImperativeCounter)

Here's how you might test this:

import * as React from 'react'
import { render, screen, act } from '@testing-library/react'
import ImperativeCounter from '../imperative-counter'

test('can call imperative methods on counter component', () => {
	const counterRef = React.createRef()
	render(<ImperativeCounter ref={counterRef} />)
	expect(screen.getByText('The count is: 0')).toBeInTheDocument()

	counterRef.current.increment()
	expect(screen.getByText('The count is: 1')).toBeInTheDocument()

	counterRef.current.decrement()
	expect(screen.getByText('The count is: 0')).toBeInTheDocument()
})

You'll get an act warning on those increment and decrement calls because they're happening outside the React callstack. So, let's wrap them in act:

import * as React from 'react'
import { render, screen, act } from '@testing-library/react'
import ImperativeCounter from '../imperative-counter'

test('can call imperative methods on counter component', () => {
	const counterRef = React.createRef()
	render(<ImperativeCounter ref={counterRef} />)
	expect(screen.getByText('The count is: 0')).toBeInTheDocument()

	act(() => counterRef.current.increment())
	expect(screen.getByText('The count is: 1')).toBeInTheDocument()

	act(() => counterRef.current.decrement())
	expect(screen.getByText('The count is: 0')).toBeInTheDocument()
})

Conclusion

Hopefully you have a better understanding (and appreciation) for React's (sometimes annoying but always right) act(...) testing utility and the next time you get a warning you're able to identify the source of the problem and fix it more quickly.

Again, most of the time you shouldn't actually have to use act directly (thanks to React Testing Library's async utilities), but when you do, you'll know why it's needed and fixes the problem.

Good luck!