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

推荐订阅源

C
Check Point Blog
AI
AI
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
U
Unit 42
Vercel News
Vercel News
Stack Overflow Blog
Stack Overflow Blog
P
Proofpoint News Feed
Microsoft Security Blog
Microsoft Security Blog
The GitHub Blog
The GitHub Blog
WordPress大学
WordPress大学
Martin Fowler
Martin Fowler
博客园 - 【当耐特】
B
Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Apple Machine Learning Research
Apple Machine Learning Research
博客园_首页
F
Full Disclosure
Google DeepMind News
Google DeepMind News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
H
Help Net Security
Recorded Future
Recorded Future
N
News and Events Feed by Topic
雷峰网
雷峰网
V
Vulnerabilities – Threatpost
Schneier on Security
Schneier on Security
aimingoo的专栏
aimingoo的专栏
S
Schneier on Security
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
O
OpenAI News
Project Zero
Project Zero
罗磊的独立博客
G
GRAHAM CLULEY
腾讯CDC
P
Privacy International News Feed
V
V2EX
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Hugging Face - Blog
Hugging Face - Blog
爱范儿
爱范儿
H
Heimdal Security Blog
L
LINUX DO - 热门话题
Forbes - Security
Forbes - Security
美团技术团队
MongoDB | Blog
MongoDB | Blog
Security Latest
Security Latest
M
MIT News - Artificial intelligence
T
Tor Project blog
Cisco Talos Blog
Cisco Talos Blog
宝玉的分享
宝玉的分享
T
Threat Research - Cisco Blogs
TaoSecurity Blog
TaoSecurity Blog

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 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 Fix the "not wrapped in act(...)" warning 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
useState lazy initialization and function updates
2020-08-03 · via Kent C. Dodds Blog

If you've been working with React for a while, you've probably used useState. Here's a quick example of the API:

function Counter() {
	const [count, setCount] = React.useState(0)
	const increment = () => setCount(count + 1)
	return <button onClick={increment}>{count}</button>
}

So, you call useState with the initial state value, and it returns an array with the value of that state and a mechanism for updating it (which is called the "state dispatch function"). When you call the state dispatch function, you pass the new value for the state and that triggers a re-render of the component which leads to useState getting called again to retrieve the new state value and the dispatch function again.

This is one of the first things you learn about state when you're getting into React (at least, it is if you learn from my free course). But there's a lesser known feature of both the useState call and the dispatch function that can be useful at times:

function Counter() {
	const [count, setCount] = React.useState(() => 0)
	const increment = () => setCount((previousCount) => previousCount + 1)
	return <button onClick={increment}>{count}</button>
}

The difference is that useState in this example is called with a function that returns the initial state (rather than simply passing the initial state) and setCount (dispatch) is called with a function that accepts the previous state value and returns the new one. This functions exactly the same as the previous example, but there are subtle differences that we'll get into in this post. I teach about this in my free React course as well, but I get asked about it enough that I thought I'd write about it as well.

useState Lazy initialization

If you were to add a console.log to the function body of the Counter function, you would find that the function is run every time you click the button. This makes sense because your Counter function is run during every render phase and clicking the button triggers the state update which triggers the re-render. One thing you need to keep in mind is that if the function body runs, that means all the code inside it runs as well. Which means any variables you create or arguments you pass are created and evaluated every render. This is normally not a big deal because JavaScript engines are very fast and can optimize for this kind of thing. So something like this would be no problem:

const initialState = 0
const [count, setCount] = React.useState(initialState)

However, what if the initial value for your state is computationally expensive?

const initialState = calculateSomethingExpensive(props)
const [count, setCount] = React.useState(initialState)

Or, more practically, what if you need to read into localStorage which is an IO operation?

const initialState = Number(window.localStorage.getItem('count'))
const [count, setCount] = React.useState(initialState)

Remember that the only time React needs the initial state is initially 😉 Meaning, it only really needs the initial state on the first render. But because our function body runs every time there's a re-render of our component, we end up running that code on every render, even if its value is not used or needed.

This is what the lazy initialization is all about. It allows you to put that code in a function:

const getInitialState = () => Number(window.localStorage.getItem('count'))
const [count, setCount] = React.useState(getInitialState)

Creating a function is fast. Even if what the function does is computationally expensive. So you only pay the performance penalty when you call the function. So if you pass a function to useState, React will only call the function when it needs the initial value (which is when the component is initially rendered).

This is called "lazy initialization." It's a performance optimization. You shouldn't have to use it a whole lot, but it can be useful in some situations, so it's good to know that it's a feature that exists and you can use it when needed. I would say I use this only 2% of the time. It's not really a feature I use often.

dispatch function updates

This one's a little bit more complicated. The easiest way to explain what it's for is by showing an example. Let's say that before we can update our count state, we need to do something async:

function DelayedCounter() {
	const [count, setCount] = React.useState(0)
	const increment = async () => {
		await doSomethingAsync()
		setCount(count + 1)
	}
	return <button onClick={increment}>{count}</button>
}

Let's say that async thing takes 500ms. That's rendered here, click this button three times really fast:

If you clicked it fast enough, you'll notice that the count was only incremented to 1. But that's weird, because if you added a console.log to the increment function, you'll find that it's called three times (once for every click), so why is the count state not updated three times?

Well, this is where things get tricky. The fact is that the state is updated three times (once for every click), but if you added a console.log(count) right above the setCount call, you'd notice that count is 0 every time! So we're calling setCount(0 + 1) for every click, even though we actually want to increment the count.

So why is the count 0 every time? It's because the increment function that we've given to React through the onClick prop on that button "closes over" the value of count at the time it's created. You can learn more about closures from mdn.io/closure, but in short, when a function is created, it has access to the variables defined outside of it and even if what those variables are assigned to changes.

The problem is that we're actually calling the exact same increment function which has closed over the value of 0 for the count before it has a chance to update based on the previous click and it stays that way until React re-renders.

So you might think we could solve this problem if we could just trigger a re-render before the async operation, but that won't do it for us either. The problem with that approach is that while increment has access to count for this render, it won't have access to the count variable for the next render. In fact, on the next render, we'll have a completely different increment function which has access to a completely different count variable. So we'll effectively have two copies of all our variables each render. (Garbage collection typically cleans up the copies and we only need to worry about the here-and-now.) But because the count hasn't been updated yet, when the new copy of increment is created, the value of count is still 0 and that's why it's 0 when we click the button the second time as well as the first.

You'll notice that if you wait for the count value to update before you click the button again, everything works fine, this is because we've waited long enough for the re-render to occur and a new increment function has been created with the latest value for count.

But obviously, this is a little confusing and it's also a little problematic. So what's the solution? Function updates! What we really need is some way to determine the previous value of count when we're making our update so we can determine it based on the previous value of count.

Any time I need to compute new state based on previous state, I use a function update.

So here's the solution:

function DelayedCounter() {
	const [count, setCount] = React.useState(0)
	const increment = async () => {
		await doSomethingAsync()
		setCount((previousCount) => previousCount + 1)
	}
	return <button onClick={increment}>{count}</button>
}

Try that here:

You can click that as frequently as you like and it'll manage updating the count for every click. This works because we no longer worry about accessing a value that may be "stale" but instead we get access to the latest value of the variable we need. So even though the increment function we're running in has an older version of count, our function updater receives the most up-to-date version of the state.

I should add that you won't have this same problem if you're using useReducer because that will always receive the most recent version of the state (as the first argument to your reducer), so you don't need to worry about stale state values. Though an exception to this would be if your state update is determined by props or some external state, in which case you might need a useRef to help ensure you've got the most current version of that prop or external state. But that's a subject for another blog post...

Conclusion

I hope that helps explain the what/why/how of useState lazy initializers and dispatch function updates. Lazy initializers are useful to improve performance issues in some scenarios, the dispatch function updates help you avoid issues with stale values.

If you enjoyed this post, you might also like How to implement useState with useReducer and Should I useState or useReducer.

Good luck!