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

推荐订阅源

Cisco Talos Blog
Cisco Talos Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
爱范儿
爱范儿
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Jina AI
Jina AI
雷峰网
雷峰网
The Register - Security
The Register - Security
The Cloudflare Blog
博客园 - 【当耐特】
M
MIT News - Artificial intelligence
I
InfoQ
博客园 - 三生石上(FineUI控件)
H
Help Net Security
博客园 - 司徒正美
Vercel News
Vercel News
WordPress大学
WordPress大学
S
SegmentFault 最新的问题
云风的 BLOG
云风的 BLOG
B
Blog
Google DeepMind News
Google DeepMind News
B
Blog RSS Feed
L
LangChain Blog
人人都是产品经理
人人都是产品经理
GbyAI
GbyAI
T
The Blog of Author Tim Ferriss
T
Tailwind CSS Blog
aimingoo的专栏
aimingoo的专栏
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Recorded Future
Recorded Future
小众软件
小众软件
Martin Fowler
Martin Fowler
罗磊的独立博客
Stack Overflow Blog
Stack Overflow Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
酷 壳 – CoolShell
酷 壳 – CoolShell
腾讯CDC
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Apple Machine Learning Research
Apple Machine Learning Research
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
博客园 - Franky
Hugging Face - Blog
Hugging Face - Blog
Y
Y Combinator Blog
V
Visual Studio Blog
F
Fortinet All Blogs
Microsoft Azure Blog
Microsoft Azure Blog
大猫的无限游戏
大猫的无限游戏
Engineering at Meta
Engineering at Meta
N
Netflix TechBlog - Medium
V
V2EX
Blog — PlanetScale
Blog — PlanetScale

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 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
Application State Management with React
2020-07-21 · via Kent C. Dodds Blog

Managing state is arguably the hardest part of any application. It's why there are so many state management libraries available and more coming around every day (and even some built on top of others... There are hundreds of "easier redux" abstractions on npm). Despite the fact that state management is a hard problem, I would suggest that one of the things that makes it so difficult is that we often over-engineer our solution to the problem.

There's one state management solution that I've personally tried to implement for as long as I've been using React, and with the release of React hooks (and massive improvements to React context) this method of state management has been drastically simplified.

We often talk about React components as lego building blocks to build our applications, and I think that when people hear this, they somehow think this excludes the state aspect. The "secret" behind my personal solution to the state management problem is to think of how your application's state maps to the application's tree structure.

One of the reasons redux was so successful was the fact that react-redux solved the prop drilling problem. The fact that you could share data across different parts of your tree by simply passing your component into some magical connect function was wonderful. Its use of reducers/action creators/etc. is great too, but I'm convinced that the ubiquity of redux is because it solved the prop drilling pain point for developers.

This is the reason that I only ever used redux on one project: I consistently see developers putting all of their state into redux. Not just global application state, but local state as well. This leads to a lot of problems, not the least of which is that when you're maintaining any state interaction, it involves interacting with reducers, action creators/types, and dispatch calls, which ultimately results in having to open many files and trace through the code in your head to figure out what's happening and what impact it has on the rest of the codebase.

To be clear, this is fine for state that is truly global, but for simple state (like whether a modal is open or form input value state) this is a big problem. To make matters worse, it doesn't scale very well. The larger your application gets, the harder this problem becomes. Sure you can hook up different reducers to manage different parts of your application, but the indirection of going through all these action creators and reducers is not optimal.

Having all your application state in a single object can also lead to other problems, even if you're not using Redux. When a React <Context.Provider> gets a new value, all the components that consume that value are updated and have to render, even if it's a function component that only cares about part of the data. That might lead to potential performance issues. (React-Redux v6 also tried to use this approach until they realized it wouldn't work right with hooks, which forced them to use a different approach with v7 to solve these issues.) But my point is that you don't have this problem if you have your state more logically separated and located in the react tree closer to where it matters.


Here's the real kicker, if you're building an application with React, you already have a state management library installed in your application. You don't even need to npm install (or yarn add) it. It costs no extra bytes for your users, it integrates with all React packages on npm, and it's already well documented by the React team. It's React itself.

React is a state management library

When you build a React application, you're assembling a bunch of components to make a tree of components starting at your <App /> and ending at your <input />s, <div />s and <button />s. You don't manage all of the low-level composite components that your application renders in one central location. Instead, you let each individual component manage that and it ends up being a really effective way to build your UI. You can do this with your state as well, and it's very likely that you do today:

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

function App() {
	return <Counter />
}

Edit React Codesandbox

Note that everything I'm talking about here works with class components as well. Hooks just make things a bit easier (especially context which we'll get into in a minute).

class Counter extends React.Component {
	state = { count: 0 }
	increment = () => this.setState(({ count }) => ({ count: count + 1 }))
	render() {
		return <button onClick={this.increment}>{this.state.count}</button>
	}
}

"Ok, Kent, sure having a single element of state managed in a single component is easy, but what do you do when I need to share that state across components? For example, what if I wanted to do this:"

function CountDisplay() {
	// where does `count` come from?
	return <div>The current counter count is {count}</div>
}

function App() {
	return (
		<div>
			<CountDisplay />
			<Counter />
		</div>
	)
}

"The count is managed inside <Counter />, now I need a state management library to access that count value from the <CountDisplay /> and update it in <Counter />!"

The answer to this problem is as old as React itself (older?) and has been in the docs for as long as I can remember: Lifting State Up

"Lifting State Up" is legitimately the answer to the state management problem in React and it's a rock solid one. Here's how you apply it to this situation:

function Counter({ count, onIncrementClick }) {
	return <button onClick={onIncrementClick}>{count}</button>
}

function CountDisplay({ count }) {
	return <div>The current counter count is {count}</div>
}

function App() {
	const [count, setCount] = React.useState(0)
	const increment = () => setCount((c) => c + 1)
	return (
		<div>
			<CountDisplay count={count} />
			<Counter count={count} onIncrementClick={increment} />
		</div>
	)
}

Edit React Codesandbox

We've just changed who's responsible for our state and it's really straightforward. And we could keep lifting state all the way to the top of our app.

"Sure Kent, ok, but what about the prop drilling problem?"

Great question. Your first line of defense against this is to change the way you're structuring your components. Take advantage of component composition. Maybe instead of:

function App() {
	const [someState, setSomeState] = React.useState('some state')
	return (
		<>
			<Header someState={someState} onStateChange={setSomeState} />
			<LeftNav someState={someState} onStateChange={setSomeState} />
			<MainContent someState={someState} onStateChange={setSomeState} />
		</>
	)
}

You could do this instead:

function App() {
	const [someState, setSomeState] = React.useState('some state')
	return (
		<>
			<Header
				logo={<Logo someState={someState} />}
				settings={<Settings onStateChange={setSomeState} />}
			/>
			<LeftNav>
				<SomeLink someState={someState} />
				<SomeOtherLink someState={someState} />
				<Etc someState={someState} />
			</LeftNav>
			<MainContent>
				<SomeSensibleComponent someState={someState} />
				<AndSoOn someState={someState} />
			</MainContent>
		</>
	)
}

If that's not super clear (because it's super contrived), Michael Jackson has a great video you can watch to help clarify what I mean.

Eventually though, even composition's not going to do it for you, so your next step is to jump into React's Context API. This has actually been a "solution" for a long time, but for a long time that solution was "unofficial." As I said, many people reached for react-redux because it solved this problem using the mechanism I'm referring to without them having to be worried about the warning that was in the React docs. But now that context is an officially supported part of the React API, we can use this directly without any problem:

import * as React from 'react'

const CountContext = React.createContext()

function useCount() {
	const context = React.useContext(CountContext)
	if (!context) {
		throw new Error(`useCount must be used within a CountProvider`)
	}
	return context
}

function CountProvider(props) {
	const [count, setCount] = React.useState(0)
	const value = React.useMemo(() => [count, setCount], [count])
	return <CountContext.Provider value={value} {...props} />
}

export { CountProvider, useCount }
import * as React from 'react'
import { CountProvider, useCount } from './count-context'

function Counter() {
	const [count, setCount] = useCount()
	const increment = () => setCount((c) => c + 1)
	return <button onClick={increment}>{count}</button>
}

function CountDisplay() {
	const [count] = useCount()
	return <div>The current counter count is {count}</div>
}

function CountPage() {
	return (
		<div>
			<CountProvider>
				<CountDisplay />
				<Counter />
			</CountProvider>
		</div>
	)
}

Edit React Codesandbox

NOTE: That particular code example is VERY contrived and I would NOT recommend you reach for context to solve this specific scenario. Please read Prop Drilling to get a better sense for why prop drilling isn't necessarily a problem and is often desirable. Don't reach for context too soon!

And what's cool about this approach is that we could put all the logic for common ways to update the state in our useCount hook:

function useCount() {
	const context = React.useContext(CountContext)
	if (!context) {
		throw new Error(`useCount must be used within a CountProvider`)
	}
	const [count, setCount] = context

	const increment = () => setCount((c) => c + 1)
	return {
		count,
		setCount,
		increment,
	}
}

Edit React Codesandbox

And you could easily change this to useReducer rather than useState as well:

function countReducer(state, action) {
	switch (action.type) {
		case 'INCREMENT': {
			return { count: state.count + 1 }
		}
		default: {
			throw new Error(`Unsupported action type: ${action.type}`)
		}
	}
}

function CountProvider(props) {
	const [state, dispatch] = React.useReducer(countReducer, { count: 0 })
	const value = React.useMemo(() => [state, dispatch], [state])
	return <CountContext.Provider value={value} {...props} />
}

function useCount() {
	const context = React.useContext(CountContext)
	if (!context) {
		throw new Error(`useCount must be used within a CountProvider`)
	}
	const [state, dispatch] = context

	const increment = () => dispatch({ type: 'INCREMENT' })
	return {
		state,
		dispatch,
		increment,
	}
}

Edit React Codesandbox

This gives you an immense amount of flexibility and reduces complexity by orders of magnitude. Here are a few important things to remember when doing things this way:

  1. Not everything in your application needs to be in a single state object. Keep things logically separated (user settings does not necessarily have to be in the same context as notifications). You will have multiple providers with this approach.
  2. Not all of your context needs to be globally accessible! Keep state as close to where it's needed as possible.

More on that second point. Your app tree could look something like this:

function App() {
	return (
		<ThemeProvider>
			<AuthenticationProvider>
				<Router>
					<Home path="/" />
					<About path="/about" />
					<UserPage path="/:userId" />
					<UserSettings path="/settings" />
					<Notifications path="/notifications" />
				</Router>
			</AuthenticationProvider>
		</ThemeProvider>
	)
}

function Notifications() {
	return (
		<NotificationsProvider>
			<NotificationsTab />
			<NotificationsTypeList />
			<NotificationsList />
		</NotificationsProvider>
	)
}

function UserPage({ username }) {
	return (
		<UserProvider username={username}>
			<UserInfo />
			<UserNav />
			<UserActivity />
		</UserProvider>
	)
}

function UserSettings() {
	// this would be the associated hook for the AuthenticationProvider
	const { user } = useAuthenticatedUser()
}

Notice that each page can have its own provider that has data necessary for the components underneath it. Code splitting "just works" for this stuff as well. How you get data into each provider is up to the hooks those providers use and how you retrieve data in your application, but you know just where to start looking to find out how that works (in the provider).

For even more about why this colocation is beneficial, check out my "State Colocation will make your React app faster" and "Colocation" blog posts. And for more about context, read How to use React Context effectively

Server Cache vs UI State

One last thing I want to add. There are various categories of state, but every type of state can fall into one of two buckets:

  1. Server Cache - State that's actually stored on the server and we store in the client for quick-access (like user data).
  2. UI State - State that's only useful in the UI for controlling the interactive parts of our app (like modal isOpen state).

We make a mistake when we combine the two. Server cache has inherently different problems from UI state and therefore needs to be managed differently. If you embrace the fact that what you have is not actually state at all but is instead a cache of state, then you can start thinking about it correctly and therefore managing it correctly.

You can definitely manage this yourself with your own useState or useReducer with the right useContext here and there. But allow me to help you cut to the chase and say that caching is a really hard problem (some say that it's one of the hardest in computer science) and it would be wise to stand on the shoulders of giants on this one.

This is why I use and recommend react-query for this kind of state. I know I know, I told you that you don't need a state management library, but I don't really consider react-query to be a state management library. I consider it to be a cache. And it's a darn good one. Give it a look! That Tanner Linsley is one smart cookie.

What about performance?

When you follow the advice above, performance will rarely be an issue. Particularly when you're following the recommendations around colocation. However, there are definitely use cases where performance can be problematic. When you have state-related performance problems, the first thing to check is how many components are being re-rendered due to a state change and determine whether those components really need to be re-rendered due to that state change. If they do, then the perf issue isn't in your mechanism for managing state, but in the speed of your render in which case you need to speed up your renders.

However, if you notice that there are a lot of components that are rendering with no DOM updates or needed side-effects, then those components are rendering unnecessarily. This happens all the time with React and it's normally not a problem by itself (and you should focus on making even unnecessary re-renders fast first), but if it really is the bottleneck, then here are a few approaches to solving performance issues with state in React context:

  1. Separate your state into different logical pieces rather than in one big store, so a single update to any part of state does NOT trigger an update to every component in your app.
  2. Optimize your context provider
  3. Bring in jotai

There it is again, another recommendation for a library. It's true, there are some use cases for which React's built-in state management abstractions are not well suited. Of all the available abstractions, jotai is the most promising for those use cases. If you're curious what those use cases are, the types of problems jotai solves well are actually really well described in Recoil: State Management for Today's React - Dave McCabe aka @mcc_abe at @ReactEurope 2020. Recoil and jotai are very similar (and solve the same types of problems). But based on my (limited) experience with them, I prefer jotai.

In any case, most apps won't need an atomic state management tool like recoil or jotai.

Conclusion

Again, this is something that you can do with class components (you don't have to use hooks). Hooks make this much easier, but you could implement this philosophy with React 15 no problem. Keep state as local as possible and use context only when prop drilling really becomes a problem. Doing things this way will make it easier for you to maintain state interactions.