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

推荐订阅源

cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
雷峰网
雷峰网
Recent Announcements
Recent Announcements
月光博客
月光博客
G
Google Developers Blog
腾讯CDC
S
Secure Thoughts
大猫的无限游戏
大猫的无限游戏
T
Tenable Blog
云风的 BLOG
云风的 BLOG
W
WeLiveSecurity
博客园 - 【当耐特】
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
博客园 - 聂微东
The Cloudflare Blog
阮一峰的网络日志
阮一峰的网络日志
人人都是产品经理
人人都是产品经理
P
Privacy International News Feed
MyScale Blog
MyScale Blog
K
Kaspersky official blog
T
The Blog of Author Tim Ferriss
Attack and Defense Labs
Attack and Defense Labs
Spread Privacy
Spread Privacy
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
aimingoo的专栏
aimingoo的专栏
I
Intezer
Vercel News
Vercel News
小众软件
小众软件
Simon Willison's Weblog
Simon Willison's Weblog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
N
Netflix TechBlog - Medium
P
Proofpoint News Feed
Latest news
Latest news
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
T
Tor Project blog
S
Security Affairs
P
Proofpoint News Feed
博客园 - 三生石上(FineUI控件)
博客园 - Franky
C
Cyber Attacks, Cyber Crime and Cyber Security
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
美团技术团队
Recent Commits to openclaw:main
Recent Commits to openclaw:main
S
Security @ Cisco Blogs
L
LINUX DO - 热门话题
Know Your Adversary
Know Your Adversary
Project Zero
Project Zero
D
Docker
L
Lohrmann on Cybersecurity
F
Full Disclosure

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 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 Profile a React App for Performance
Don't Sync State. Derive It!
2019-09-30 · via Kent C. Dodds Blog

In my Learn React Hooks workshop material, we have an exercise where we build a tic-tac-toe game using React's useState hook (based on the official React tutorial). Here's the Github file for the finished version of that exercise

We have a few variables of state. There's a squares state variable via React.useState. There's also nextValue, winner, and status are each determined by calling the functions calculateNextValue, calculateWinner, and calculateStatus. squares is regular component state, but nextValue, winner, and status are what are called "derived state." That means that their value can be derived (or calculated) based on other values rather than managed on their own.

There's a good reason that I wrote it the way I did. Let's find out the benefits of derived state over state synchronization by reimplementing this with a more naive approach. The fact is that all four variables are technically state so you may automatically think that you need to use useState or useReducer for them.

Let's start with useState:

function Board() {
	const [squares, setSquares] = React.useState(Array(9).fill(null))
	const [nextValue, setNextValue] = React.useState(calculateNextValue(squares))
	const [winner, setWinner] = React.useState(calculateWinner(squares))
	const [status, setStatus] = React.useState(calculateStatus(squares))

	function selectSquare(square) {
		if (winner || squares[square]) {
			return
		}
		const squaresCopy = [...squares]
		squaresCopy[square] = nextValue
		const newNextValue = calculateNextValue(squaresCopy)
		const newWinner = calculateWinner(squaresCopy)
		const newStatus = calculateStatus(newWinner, squaresCopy, newNextValue)
		setSquares(squaresCopy)
		setNextValue(newNextValue)
		setWinner(newWinner)
		setStatus(newStatus)
	}

	// return beautiful JSX
}

So that's not all that bad. Where it becomes a real problem is what if we added a feature to our tic-tac-toe game where you could select two squares at once? What would we have to do to make that happen?

function Board() {
	const [squares, setSquares] = React.useState(Array(9).fill(null))
	const [nextValue, setNextValue] = React.useState(calculateNextValue(squares))
	const [winner, setWinner] = React.useState(calculateWinner(squares))
	const [status, setStatus] = React.useState(calculateStatus(squares))

	function selectSquare(square) {
		if (winner || squares[square]) {
			return
		}
		const squaresCopy = [...squares]
		squaresCopy[square] = nextValue

		const newNextValue = calculateNextValue(squaresCopy)
		const newWinner = calculateWinner(squaresCopy)
		const newStatus = calculateStatus(newWinner, squaresCopy, newNextValue)
		setSquares(squaresCopy)
		setNextValue(newNextValue)
		setWinner(newWinner)
		setStatus(newStatus)
	}

	function selectTwoSquares(square1, square2) {
		if (winner || squares[square1] || squares[square2]) {
			return
		}
		const squaresCopy = [...squares]
		squaresCopy[square1] = nextValue
		squaresCopy[square2] = nextValue

		const newNextValue = calculateNextValue(squaresCopy)
		const newWinner = calculateWinner(squaresCopy)
		const newStatus = calculateStatus(newWinner, squaresCopy, newNextValue)
		setSquares(squaresCopy)
		setNextValue(newNextValue)
		setWinner(newWinner)
		setStatus(newStatus)
	}

	// return beautiful JSX
}

The biggest problem with this is some of that state may fall out of sync with the true component state (squares). It could fall out of sync because we forget to update it for a complex sequence of interactions for example. If you've been building React apps for a while, you know what I'm talking about. It's no fun to have things fall out of sync.

One thing that can help is to reduce duplication so that all relevant state updates happen in one place:

function Board() {
	const [squares, setSquares] = React.useState(Array(9).fill(null))
	const [nextValue, setNextValue] = React.useState(calculateNextValue(squares))
	const [winner, setWinner] = React.useState(calculateWinner(squares))
	const [status, setStatus] = React.useState(calculateStatus(squares))

	function setNewState(newSquares) {
		const newNextValue = calculateNextValue(newSquares)
		const newWinner = calculateWinner(newSquares)
		const newStatus = calculateStatus(newWinner, newSquares, newNextValue)
		setSquares(newSquares)
		setNextValue(newNextValue)
		setWinner(newWinner)
		setStatus(newStatus)
	}

	function selectSquare(square) {
		if (winner || squares[square]) {
			return
		}
		const squaresCopy = [...squares]
		squaresCopy[square] = nextValue
		setNewState(squaresCopy)
	}

	function selectTwoSquares(square1, square2) {
		if (winner || squares[square1] || squares[square2]) {
			return
		}
		const squaresCopy = [...squares]
		squaresCopy[square1] = nextValue
		squaresCopy[square2] = nextValue
		setNewState(squaresCopy)
	}

	// return beautiful JSX
}

That's really improved our code duplication, and it wasn't that big of a deal honestly. But this is a pretty simple example. Sometimes the derived state is based on multiple variables of state that are updated in different situations and we need to make sure that all our state is updated whenever the source state is updated.

The solution

What if I told you there's something better? If you've already read through the codesandbox implementation above, you know what that solution is, but let's put it right here now:

function Board() {
	const [squares, setSquares] = React.useState(Array(9).fill(null))
	const nextValue = calculateNextValue(squares)
	const winner = calculateWinner(squares)
	const status = calculateStatus(winner, squares, nextValue)

	function selectSquare(square) {
		if (winner || squares[square]) {
			return
		}
		const squaresCopy = [...squares]
		squaresCopy[square] = nextValue
		setSquares(squaresCopy)
	}

	// return beautiful JSX
}

Nice! We don't need to worry about updating the derived state values because they're simply calculated every render. Cool. Let's add that two squares at a time feature:

function Board() {
	const [squares, setSquares] = React.useState(Array(9).fill(null))
	const nextValue = calculateNextValue(squares)
	const winner = calculateWinner(squares)
	const status = calculateStatus(winner, squares, nextValue)

	function selectSquare(square) {
		if (winner || squares[square]) {
			return
		}
		const squaresCopy = [...squares]
		squaresCopy[square] = nextValue
		setSquares(squaresCopy)
	}

	function selectTwoSquares(square1, square2) {
		if (winner || squares[square1] || squares[square2]) {
			return
		}
		const squaresCopy = [...squares]
		squaresCopy[square1] = nextValue
		squaresCopy[square2] = nextValue
		setSquares(squaresCopy)
	}

	// return beautiful JSX
}

Sweet! Before we had to concern ourselves with every single time we updated the squares state to ensure we updated all of the other state properly as well. But now we don't need to worry about it at all. It just works. No need for a fancy function to handle updating all the derived state. We just calculate it on the fly.

What about useReducer?

useReducer doesn't suffer as badly from these problems. Here's how I might implement this using useReducer:

function calculateDerivedState(squares) {
	const winner = calculateWinner(squares)
	const nextValue = calculateNextValue(squares)
	const status = calculateStatus(winner, squares, nextValue)
	return { squares, nextValue, winner, status }
}

function ticTacToeReducer(state, square) {
	if (state.winner || state.squares[square]) {
		// no state change needed.
		// (returning the same object allows React to bail out of a re-render)
		return state
	}

	const squaresCopy = [...state.squares]
	squaresCopy[square] = state.nextValue

	return { ...calculateDerivedState(squaresCopy), squares: squaresCopy }
}

function Board() {
	const [{ squares, status }, selectSquare] = React.useReducer(
		ticTacToeReducer,
		Array(9).fill(null),
		calculateDerivedState,
	)

	// return beautiful JSX
}

This isn't the only way to do this, but the point here is that while we do still "derive" state for winner, nextValue, and status, we're managing all of that within the reducer which is the only place state updates can happen, so falling out of sync is less likely.

That said, I find this to be a little more complex than our other solution (especially if we want to add that "two squares at a time" feature). So if I were building and shipping this in a production app, I'd go with what I've got in that codesandbox.

Derived state via props

State doesn't have to be managed internally to suffer from the state synchronization problems. What if we had the squares state coming from a parent component? How would we synchronize that state?

function Board({ squares, onSelectSquare }) {
	const [nextValue, setNextValue] = React.useState(calculateNextValue(squares))
	const [winner, setWinner] = React.useState(calculateWinner(squares))
	const [status, setStatus] = React.useState(calculateStatus(squares))

	// ... hmmm... we're no longer managing updating the squares state, so how
	// do we keep these variables up to date? useEffect? useLayoutEffect?
	// React.useEffect(() => {
	//   setNextValue... etc... eh...
	// }, [squares])
	//
	// Just call the state updaters when squares change
	// right in the render method?
	// if (prevSquares !== squares) {
	//   setNextValue... etc... ugh...
	// }
	//
	// I've seen people do all of these things... And none of them are great.

	// return beautiful JSX
}

The better way to do this is just to calculate it on the fly:

function Board({ squares, onSelectSquare }) {
	const nextValue = calculateNextValue(squares)
	const winner = calculateWinner(squares)
	const status = calculateStatus(squares)

	// return beautiful JSX
}

It's way simpler, and it works really well.

P.S. Remember getDerivedStateFromProps? Well you probably don't need it but if you do and you want to do so with hooks, then calling the state updater function during render is actually the correct way to do it. Learn more from the React Hooks FAQ.

What about performance?

I know you've been waiting for me to address this... Here's the deal. JavaScript is really fast. I ran a benchmark on the calculateWinner function and this resulted in 15 MILLION operations per second. So unless your tic-tac-toe players are extremely fast at clicking around, there's no way this is going to be a performance problem (and even if they could play that fast, I assure you that you'll have other performance problems that will be lower hanging fruit for you).

Ok ok, I tried it on my phone and only got 4.3 million operations per second. And then I tried with a CPU 6x slowdown on my laptop and only got 2 million... I think we're still good.

That said, if you do happen to have a function which is computationally expensive, then that's what useMemo is for!

function Board() {
	const [squares, setSquares] = React.useState(Array(9).fill(null))
	const nextValue = React.useMemo(() => calculateNextValue(squares), [squares])
	const winner = React.useMemo(() => calculateWinner(squares), [squares])
	const status = React.useMemo(
		() => calculateStatus(winner, squares, nextValue),
		[winner, squares, nextValue],
	)

	// return beautiful JSX
}

So there you go. An escape hatch for you to use once you've determined that some code is actually computationally expensive for your users to run. Note that this doesn't magically make those functions run faster. All it does is ensure that they're not called unnecessarily. If this were our whole app, the only way for the app to re-render is if squares changes in which case all of those functions will be run anyway, so we've actually not accomplished much with this "optimization." That's why I say: "Measure first!"

Learn more about useMemo and useCallback

Oh, and I'd like to mention that derived state can sometimes be even faster than state synchronization because it will result in fewer unnecessary re-renders, which can be a problem sometimes.

What about MobX/Reselect?

Reselect (which you should absolutely be using if you're using Redux) has memoization built-in which is cool. MobX has this as well, but they also take it a step further with "computed values" which is basically an API to give you memoized and optimized derived state values. What makes it even better than what we already have is that the computation is only processed when it's accessed.

For (contrived) example:

function FavoriteNumber() {
	const [name, setName] = React.useState('')
	const [number, setNumber] = React.useState(0)
	const numberWarning = getNumberWarning(number)
	return (
		<div>
			<label>
				Your name: <input onChange={(e) => setName(e.target.value)} />
			</label>
			<label>
				Your favorite number:{' '}
				<input
					type="number"
					onChange={(e) => setNumber(Number(e.target.value))}
				/>
			</label>
			<div>
				{name
					? `${name}'s favorite number is ${number}`
					: 'Please type your name'}
			</div>
			<div>{number > 10 ? numberWarning : null}</div>
			<div>{number < 0 ? numberWarning : null}</div>
		</div>
	)
}

Notice that we're calling getNumberWarning, but we're only using the result if the number is too high or too low, so we may not actually need to call that function at all. Now, it's unlikely this is problematic, but let's say for the sake of argument that calling getNumberWarning is an application bottleneck. This is where the computed values feature comes in handy.

If you're experiencing this a lot in your app, then I suggest you just jump into using MobX (MobX folks will tell you there are a lot of other reasons to use it as well), but we can solve this specific situation pretty easily ourselves:

function FavoriteNumber() {
	const [name, setName] = React.useState('')
	const [number, setNumber] = React.useState(0)
	const numberIsTooHigh = number > 10
	const numberIsTooLow = number < 0
	const numberWarning =
		numberIsTooHigh || numberIsTooLow ? getNumberWarning(number) : null
	return (
		<div>
			<label>
				Your name: <input onChange={(e) => setName(e.target.value)} />
			</label>
			<label>
				Your favorite number:{' '}
				<input
					type="number"
					onChange={(e) => setNumber(Number(e.target.value))}
				/>
			</label>
			<div>
				{name
					? `${name}'s favorite number is ${number}`
					: 'Please type your name'}
			</div>
			<div>{numberIsTooHigh ? numberWarning : null}</div>
			<div>{numberIsTooLow ? numberWarning : null}</div>
		</div>
	)
}

Great! Now we don't need to worry about calling numberWarning when it's not needed. But if that doesn't work well for your situation, then we could make a custom hook do this magic for us. It's not exactly simple and it's a bit of a hack (there's probably a better way to do it honestly), so I'm just going to put this in a codesandbox and let you explore it if you want:

Error embedding https://codesandbox.io/s/usecomputedvalue-custom-hook-mo5b4

It's sufficient to say that the custom hook allows us to do this:

function FavoriteNumber() {
	const [name, setName] = React.useState('')
	const [number, setNumber] = React.useState(0)
	const numberWarning = useComputedValue(
		() => getNumberWarning(number),
		[number],
	)
	return (
		<div>
			<label>
				Your name: <input onChange={(e) => setName(e.target.value)} />
			</label>
			<label>
				Your favorite number:{' '}
				<input
					type="number"
					onChange={(e) => setNumber(Number(e.target.value))}
				/>
			</label>
			<div>
				{name
					? `${name}'s favorite number is ${number}`
					: 'Please type your name'}
			</div>
			<div>{number > 10 ? numberWarning.result : null}</div>
			<div>{number < 0 ? numberWarning.result : null}</div>
		</div>
	)
}

And our getNumberWarning function is only called when the result is actually used. Think of it like a useMemo that only runs the callback when the return value is rendered.

I think there may be room to perfect and open source that. Feel free to do so and then make a PR to this blog post to add a link to your published package 😉

Again, there's really not much reason to worry yourself over this kind of thing in a normal scenario. But if you do have perf bottlenecks around and useMemo isn't enough for you, then consider doing something like this or use MobX.

Conclusion

Ok, so we got a little distracted overthinking performance for a second there. The fact is that you can really simplify your app's state by considering whether the state needs to be managed by itself or if it can be derived. We learned that derived state can be the result of a single variable of state, or it can be derived from multiple variables of state (some of which can also be derived state itself).

So next time you're maintaining the state of your app and trying to figure out a synchronization bug, think about how you could make it derived on the fly instead. And in the few instances you bump into performance issues you can reach to a few optimization strategies to help alleviate some of that pain. Good luck!