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

推荐订阅源

Recent Commits to openclaw:main
Recent Commits to openclaw:main
L
LangChain Blog
月光博客
月光博客
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 【当耐特】
宝玉的分享
宝玉的分享
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Last Week in AI
Last Week in AI
人人都是产品经理
人人都是产品经理
博客园_首页
T
Tailwind CSS Blog
P
Proofpoint News Feed
雷峰网
雷峰网
D
Darknet – Hacking Tools, Hacker News & Cyber Security
IT之家
IT之家
V
Vulnerabilities – Threatpost
阮一峰的网络日志
阮一峰的网络日志
C
CERT Recently Published Vulnerability Notes
Attack and Defense Labs
Attack and Defense Labs
S
Schneier on Security
Security Archives - TechRepublic
Security Archives - TechRepublic
L
Lohrmann on Cybersecurity
V
Visual Studio Blog
云风的 BLOG
云风的 BLOG
WordPress大学
WordPress大学
The Register - Security
The Register - Security
N
Netflix TechBlog - Medium
Hugging Face - Blog
Hugging Face - Blog
Project Zero
Project Zero
博客园 - 叶小钗
F
Full Disclosure
大猫的无限游戏
大猫的无限游戏
Latest news
Latest news
S
SegmentFault 最新的问题
C
Cyber Attacks, Cyber Crime and Cyber Security
Google Online Security Blog
Google Online Security Blog
Recorded Future
Recorded Future
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Hacker News - Newest:
Hacker News - Newest: "LLM"
腾讯CDC
L
LINUX DO - 最新话题
Google DeepMind News
Google DeepMind News
P
Privacy International News Feed
I
InfoQ
F
Fortinet All Blogs
Vercel News
Vercel News
H
Hackread – Cybersecurity News, Data Breaches, AI and More
T
Threatpost
T
Tenable Blog
B
Blog RSS Feed

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 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
Should I useState or useReducer?
2020-03-09 · via Kent C. Dodds Blog

Watch "When to useState instead of useReducer" on egghead.io

Watch "When to useReducer instead of useState" on egghead.io

Whenever there are two things to do the same thing, people inevitably ask: "When do I use one over the other?" There are two possible reasons for having multiple ways of doing the same thing:

  1. One is the "old way" and the other is the "new (and improved) way". Typically the old way is kept around for backward compatibility reasons and the new way is the path forward for new code. For example: class components (old way) vs function components (new way).
  2. They come with different trade-offs that should be considered and therefore should be applied in situations that suit them better (sometimes meaning you'll use more than one in a given application). For example: useEffect vs useLayoutEffect or Static vs Unit vs Integration vs E2E tests or "Control Props" vs "State Reducers"

useState and useReducer fall into the second category here. So let's talk about the trade-offs.

(and no, it's not a trick question 😂).

Examples

I think the best way to discuss these trade-offs is through the lens of examples. We'll look at two examples. One which suits useState better, and one which suits useReducer better. This won't be enough to cover all of the trade-offs, but hopefully can be a good starting point for us.

Custom useDarkMode hook

I recently wrote this for my workshop projects (for example). It's pretty interesting, so let's compare useState and useReducer implementations for that:

useState implementation

I'll highlight the areas where we're interacting with the state:

function useDarkMode() {
	const preferDarkQuery = '(prefers-color-scheme: dark)'
	const [mode, setMode] = React.useState(
		() =>
			window.localStorage.getItem('colorMode') ||
			(window.matchMedia(preferDarkQuery).matches ? 'dark' : 'light'),
	)

	React.useEffect(() => {
		const mediaQuery = window.matchMedia(preferDarkQuery)
		const handleChange = () => setMode(mediaQuery.matches ? 'dark' : 'light')
		mediaQuery.addListener(handleChange)
		return () => mediaQuery.removeListener(handleChange)
	}, [])

	React.useEffect(() => {
		window.localStorage.setItem('colorMode', mode)
	}, [mode])

	return [mode, setMode]
}

Hopefully that makes sense. Basically we're saying, if the user's set their preferences to dark mode, then we'll initialize our mode to dark, otherwise we'll initialize to light and then return the mode and setMode and as the mode changes (whether by calling setMode directly or as the user changes their system preferences) we'll keep that value set in localStorage for future use.

useReducer implementation:

There are several ways you could write this with useReducer. I'll start out with the typical way most people write reducers:

const preferDarkQuery = '(prefers-color-scheme: dark)'

function darkModeReducer(state, action) {
	switch (action.type) {
		case 'MEDIA_CHANGE': {
			return { ...state, mode: action.mode }
		}
		case 'SET_MODE': {
			// make sure to spread that state just in case!
			return { ...state, mode: action.mode }
		}
		default: {
			// helps us avoid typos!
			throw new Error(`Unhandled action type: ${action.type}`)
		}
	}
}

// use the init function to lazily initialize state so we don't read into
// localstorage or call matchMedia every render
function init() {
	return {
		mode:
			window.localStorage.getItem('colorMode') ||
			(window.matchMedia(preferDarkQuery).matches ? 'dark' : 'light'),
	}
}

function useDarkMode() {
	const [state, dispatch] = React.useReducer(
		darkModeReducer,
		{ mode: 'light' },
		init,
	)
	const { mode } = state

	React.useEffect(() => {
		const mediaQuery = window.matchMedia(preferDarkQuery)
		const handleChange = () =>
			dispatch({
				type: 'MEDIA_CHANGE',
				mode: mediaQuery.matches ? 'dark' : 'light',
			})
		mediaQuery.addListener(handleChange)
		return () => mediaQuery.removeListener(handleChange)
	}, [])

	React.useEffect(() => {
		window.localStorage.setItem('colorMode', mode)
	}, [mode])

	// We like the API the way it is, so instead of returning the state object
	// and the dispatch function, we'll return the `mode` property and we'll
	// create a setMode helper (which we have to memoize in case someone wants
	// to use it in a dependency list):
	const setMode = React.useCallback(
		(newMode) => dispatch({ type: 'SET_MODE', mode: newMode }),
		[],
	)

	return [mode, setMode]
}

Wow, I think we can both agree that the useState version was WAY simpler! But wait! We can drastically simplify the useReducer version by going against the "grain" and not writing your typical redux-style reducer. Let's try that:

function useDarkMode() {
	const preferDarkQuery = '(prefers-color-scheme: dark)'
	const [mode, setMode] = React.useReducer(
		(prevMode, nextMode) =>
			typeof nextMode === 'function' ? nextMode(prevMode) : nextMode,
		'light',
		() =>
			window.localStorage.getItem('colorMode') ||
			(window.matchMedia(preferDarkQuery).matches ? 'dark' : 'light'),
	)

	React.useEffect(() => {
		const mediaQuery = window.matchMedia(preferDarkQuery)
		const handleChange = () => setMode(mediaQuery.matches ? 'dark' : 'light')
		mediaQuery.addListener(handleChange)
		return () => mediaQuery.removeListener(handleChange)
	}, [])

	React.useEffect(() => {
		window.localStorage.setItem('colorMode', mode)
	}, [mode])

	return [mode, setMode]
}

That's a lot better than our other try with useReducer. But we basically implemented useState with useReducer. And even then, it's still less clear than our useState version. So at the end of the day, useState was a much better solution in this case.

When it's just an independent element of state you're managing: useState

Custom useUndo hook

There's a great useUndo hook on GitHub. I took inspiration from that for this example. Thank you Homer Chen!

(For these, almost everything is interacting with state, so... no highlights...)

useState implementation

function useUndo(initialPresent) {
	const [past, setPast] = React.useState([])
	const [present, setPresent] = React.useState(initialPresent)
	const [future, setFuture] = React.useState([])

	const canUndo = past.length !== 0
	const canRedo = future.length !== 0

	const undo = React.useCallback(() => {
		if (!canUndo) return

		const previous = past[past.length - 1]
		const newPast = past.slice(0, past.length - 1)

		setPast(newPast)
		setPresent(previous)
		setFuture([present, ...future])
	}, [canUndo, future, past, present])

	const redo = React.useCallback(() => {
		if (!canRedo) return

		const next = future[0]
		const newFuture = future.slice(1)

		setPast([...past, present])
		setPresent(next)
		setFuture(newFuture)
	}, [canRedo, future, past, present])

	const set = React.useCallback(
		(newPresent) => {
			if (newPresent === present) {
				return
			}
			setPast([...past, present])
			setPresent(newPresent)
			setFuture([])
		},
		[past, present],
	)

	const reset = React.useCallback((newPresent) => {
		setPast([])
		setPresent(newPresent)
		setFuture([])
	}, [])

	return [
		{ past, present, future },
		{ set, reset, undo, redo, canUndo, canRedo },
	]
}

Looks pretty ok right? It probably is, but there's actually a situation where this could be pretty buggy. But first, I want to address a common misconception people have about calling multiple state updaters in sequence (like we're doing in each of those functions).

Often people think this means that you'll trigger a re-render for each call (so, they're suggesting that calling reset would trigger three rerenders). First, remember to focus on Fixing the slow render before you fix the re-render, but secondly, remember that React has a batch system so if you were to call reset from an event handler or in a useEffect callback, it would trigger only one re-render.

That said, if we were to call reset in an async function (like after making an HTTP request), then that would result in three re-renders. However, in the future with concurrent mode those will be batched as well. So my main concern isn't the re-renders.

My concern is with the insidious stale closure bugs in our code! Can you spot them? There are three! I'll give you a hint, there's one in each of undo, redo, and set, but there's not one in reset.

Here's a contrived example that would reveal this bug:

function Example() {
	const [state, { set }] = useUndo('first')

	React.useEffect(() => {
		set('second')
	}, [])

	React.useEffect(() => {
		set('third')
	}, [])

	return <pre>{JSON.stringify(state, null, 2)}</pre>
}

The printed result here would be:

{
	"past": ["first"],
	"present": "third",
	"future": []
}

It should be:

{
	"past": ["first", "second"],
	"present": "third",
	"future": []
}

So what happened to "second" in our situation? Ah! Turns out we're missing our dependency on set in the effect dependency array. Silly goose. Let's add those:

function Example() {
	const [state, { set }] = useUndo('first')

	React.useEffect(() => {
		set('second')
	}, [set])

	React.useEffect(() => {
		set('third')
	}, [set])

	return <pre>{JSON.stringify(state, null, 2)}</pre>
}

Great, save, reload... Wait wut? Oh no! Here's what happened when I added those:

{
	"past": [
		"first",
		"second",
		"third",
		"second",
		"third",
		"second",
		"third",
		"second",
		"third",
		"second",
		"third",
		"second",
		"third",
		"second",
		"third",
		"second",
		"third",
		"second",
		"third",
		"second",
		"third",
		"second",
		"third",
		"second",
		"third",
		"second",
		"third",
		"... this goes on forever..."
	],
	"present": "third",
	"future": []
}

But wait! Aren't we memoizing the set function? It shouldn't change unless its dependencies change... Wait... That includes the past and present values... And whoops! When we call set those values are changed, which leads to our infinite loop!

Now, this may seem contrived, but a similar bug could come up if you were updating the state based on network events and they came back in a different order from when they were sent out. Either way, you just don't want to have to think about this kind of thing right? Right.

So we can fix this problem with useReducer, but we can actually change our useState implementation and side-step this issue and I thought you'd enjoy that, so here it is:

function useUndo(initialPresent) {
	const [state, setState] = React.useState({
		past: [],
		present: initialPresent,
		future: [],
	})

	const canUndo = state.past.length !== 0
	const canRedo = state.future.length !== 0

	const undo = React.useCallback(() => {
		setState((currentState) => {
			const { past, present, future } = currentState

			if (past.length === 0) return currentState

			const previous = past[past.length - 1]
			const newPast = past.slice(0, past.length - 1)

			return {
				past: newPast,
				present: previous,
				future: [present, ...future],
			}
		})
	}, [])

	const redo = React.useCallback(() => {
		setState((currentState) => {
			const { past, present, future } = currentState

			if (future.length === 0) return currentState

			const next = future[0]
			const newFuture = future.slice(1)

			return {
				past: [...past, present],
				present: next,
				future: newFuture,
			}
		})
	}, [])

	const set = React.useCallback((newPresent) => {
		setState((currentState) => {
			const { present, past } = currentState
			if (newPresent === present) return currentState

			return {
				past: [...past, present],
				present: newPresent,
				future: [],
			}
		})
	}, [])

	const reset = React.useCallback((newPresent) => {
		setState(() => ({
			past: [],
			present: newPresent,
			future: [],
		}))
	}, [])

	return [state, { set, reset, undo, redo, canUndo, canRedo }]
}

There are a few things I did to fix this issue:

  1. I used state updater callbacks in when calling the state updater functions so I could receive the currentState as an argument. This meant that I didn't need to list the state as a dependency.
  2. I combined all the state into a single object. I had to do this because there are situations where you need one value to determine another. For example, in redo, I need the value of present to update past and the value of future to update present.
  3. I did the calculations for canUndo and canRedo within the state updater callbacks based on the currentState I receive from the arguments so I didn't need to list those in the dependency array.

That solves the issue we were having and it does so pretty well, but let's try this same thing with useReducer to compare.

useReducer implementation

const UNDO = 'UNDO'
const REDO = 'REDO'
const SET = 'SET'
const RESET = 'RESET'

function undoReducer(state, action) {
	const { past, present, future } = state
	const { type, newPresent } = action

	switch (type) {
		case UNDO: {
			if (past.length === 0) return state

			const previous = past[past.length - 1]
			const newPast = past.slice(0, past.length - 1)

			return {
				past: newPast,
				present: previous,
				future: [present, ...future],
			}
		}

		case REDO: {
			if (future.length === 0) return state

			const next = future[0]
			const newFuture = future.slice(1)

			return {
				past: [...past, present],
				present: next,
				future: newFuture,
			}
		}

		case SET: {
			if (newPresent === present) return state

			return {
				past: [...past, present],
				present: newPresent,
				future: [],
			}
		}

		case RESET: {
			return {
				past: [],
				present: newPresent,
				future: [],
			}
		}
	}
}

function useUndo(initialPresent) {
	const [state, dispatch] = React.useReducer(undoReducer, {
		past: [],
		present: initialPresent,
		future: [],
	})

	const canUndo = state.past.length !== 0
	const canRedo = state.future.length !== 0
	const undo = React.useCallback(() => dispatch({ type: UNDO }), [])
	const redo = React.useCallback(() => dispatch({ type: REDO }), [])
	const set = React.useCallback(
		(newPresent) => dispatch({ type: SET, newPresent }),
		[],
	)
	const reset = React.useCallback(
		(newPresent) => dispatch({ type: RESET, newPresent }),
		[],
	)

	return [state, { set, reset, undo, redo, canUndo, canRedo }]
}

Wow, the useUndo thing itself is actually very simple now. If we had started with useReducer from the get-go, we wouldn't have even considered adding anything to our dependency array because those functions are so simple they don't need anything. All the logic lives in our reducer. That helps us avoid the issue naturally.

You may find it interesting that the switch cases in our reducer are basically exactly what the contents of our functions were before we made the change.

When one element of your state relies on the value of another element of your state in order to update: useReducer

Conclusion

So if you want some "rules" (NOT ESLINT RULES), here they are:

  • When it's just an independent element of state you're managing: useState
  • When one element of your state relies on the value of another element of your state in order to update: useReducer

Outside of these "rules," everything else is really subjective. Honestly, even the "rules" are subjective because as I demonstrated, you can do everything you want with either one.

Also, please note that this applies on a case-by-case basis. You can absolutely use useState in the same component or hook that's using useReducer. And you can have multiple useStates and multiple useReducers in a single hook or component. That's no problem. Separate state logically by domain. If it changes together, it's likely better to keep together in a reducer. If something is pretty independent from other elements of state in that hook/component, then putting it with other elements of state is just adding unnecessary complexity and noise to that reducer and you'd be better off leaving it out on its own.

So it's not just "when I have more than X number of useStates I switch to useReducer." It's more nuanced than that. But hopefully this post helps you understand those nuances and reach for the tool that has the trade-offs that work best for your situation. In general, I suggest starting with useState, and moving to useReducer when you notice elements of state need to change together.

Good luck!