💼 React State Management
React state management is the most essential decision for entrepreneurs to build scalable, performant, and robust React applications. It ensures that our app remains in sync with the user interface.
Introduction
What is State?
State is an object that holds data which changes over time in an application. In React, state provides a mechanism for components to manage and track changing data, triggering re-renders when updated.
When to Use State
In React apps, components display or operate on data—such as user input, API responses, or other dynamic content. When you need to store and update data inside a component, use state.
Why is State Management Important?
State represents the data that changes over time in your application. Proper state management ensures your app functions correctly, helps avoid data inconsistency, prop drilling, and unnecessary re-renders. As your application grows, managing state efficiently becomes critical for maintainability, scalability, and performance.
Different Approaches to Managing State in React
React state can be categorized into several types:
- Local (UI) state: Managed within a single component.
- Global state: Shared across multiple components.
- Server state: Data fetched from an external server, often with caching and synchronization.
- URL state: Data stored in URLs, such as query parameters and route params.
Component (Local) State
Each React component has its own internal state, managed with the useState
hook. This state is specific to the component and updated with setState
(or setCount
, etc.), triggering a re-render.
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
const increment = () => setCount(count + 1)
const decrement = () => setCount(count - 1)
return (
<div>
<h1>{count}</h1>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
)
}
Local state is great for isolated data, but sharing state between many components can become challenging.
Global State
Global state is accessible to every component in the application. It is typically declared in a root or provider component. Use global state when multiple components need to access or update the same data, such as authentication, user preferences, or theme.
React Context API
React’s Context API is a built-in solution for sharing state across components without prop drilling. It's ideal for smaller apps or sharing state between a few related components. However, overusing Context can lead to performance issues, as it is not optimized for high-frequency updates.
As Sebastian Markbage notes, Context is best for low-frequency updates like themes or authentication.
Example:
import { createContext, useContext, useState } from 'react'
const ThemeContext = createContext()
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light')
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
)
}
function ThemedButton() {
const { theme, setTheme } = useContext(ThemeContext)
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Current theme: {theme}
</button>
)
}
React State Management Libraries
As applications grow, you may need more advanced state management. Libraries can be grouped as follows:
- Reducer-based: Centralized state, updated via dispatched actions (e.g., Redux, Zustand).
- Atom-based: State is split into small, independent atoms (e.g., Recoil, Jotai).
- Mutable-based: Uses proxies for mutable, reactive state (e.g., MobX, Valtio).
Reducer-based Libraries
Redux & Redux Toolkit
Redux is a classic state container for React. State changes only via dispatched actions, handled by reducers. Redux Toolkit simplifies setup and reduces boilerplate.
import { createSlice, configureStore } from '@reduxjs/toolkit'
const exampleSlice = createSlice({
name: 'example',
initialState: { isAvailable: true },
reducers: {
makeAvailable: (state) => {
state.isAvailable = true
},
makeUnavailable: (state) => {
state.isAvailable = false
},
},
})
const store = configureStore({ reducer: { example: exampleSlice.reducer } })
// Usage in components:
const isAvailable = useSelector((state) => state.example.isAvailable)
const dispatch = useDispatch()
dispatch(exampleSlice.actions.makeAvailable())
Rematch
Rematch is a Redux abstraction that reduces boilerplate and simplifies async logic.
const countModel = {
state: 0,
reducers: {
increment: (state, payload) => state + payload,
},
effects: (dispatch) => ({
async incrementAsync(payload) {
await new Promise((resolve) => setTimeout(resolve, 1000))
dispatch.count.increment(payload)
},
}),
}
Zustand
Zustand is a minimal, hook-based state management library.
import { create } from 'zustand'
const useStore = create((set, get) => ({
isAvailable: true,
status: () => (get().isAvailable ? 'Available' : 'Unavailable'),
makeAvailable: () => set({ isAvailable: true }),
makeUnavailable: () => set({ isAvailable: false }),
}))
const { isAvailable, makeAvailable } = useStore()
You can also combine Immer with Zustand to handle nested objects and complicated data.
Atom-based Libraries
Recoil
Recoil introduces atoms (units of state) and selectors (derived/computed state).
import { atom, selector, useRecoilState, useRecoilValue } from 'recoil'
const isAvailableState = atom({ key: 'isAvailableState', default: true })
const statusState = selector({
key: 'statusState',
get: ({ get }) => (get(isAvailableState) ? 'Available' : 'Unavailable'),
})
const [isAvailable, setIsAvailable] = useRecoilState(isAvailableState)
const status = useRecoilValue(statusState)
Jotai
Jotai is a minimal atomic state library using JavaScript's WeakMap for efficient memory management.
import { atom, useAtom } from 'jotai'
const isAvailableState = atom(true)
const statusState = atom((get) =>
get(isAvailableState) ? 'Available' : 'Unavailable',
)
const [isAvailable, setIsAvailable] = useAtom(isAvailableState)
const [status] = useAtom(statusState)
Mutable-based Libraries
MobX
MobX uses observables and OOP patterns for state management.
import { makeObservable, observable, computed } from 'mobx'
class TodoList {
todos = []
get unfinishedTodoCount() {
return this.todos.filter((todo) => !todo.finished).length
}
constructor(todos) {
makeObservable(this, {
todos: observable,
unfinishedTodoCount: computed,
})
this.todos = todos
}
}
import { observer } from 'mobx-react-lite'
const TodoListView = observer(({ todoList }) => (
<div>
<ul>
{todoList.todos.map((todo) => (
<TodoView todo={todo} key={todo.id} />
))}
</ul>
Tasks left: {todoList.unfinishedTodoCount}
</div>
))
Valtio
Valtio uses proxies for fine-grained reactivity.
import { proxy, useSnapshot } from 'valtio'
const state = proxy({ count: 0, text: 'hello' })
function Counter() {
const snap = useSnapshot(state)
return (
<div>
{snap.count}
<button onClick={() => ++state.count}>+1</button>
</div>
)
}
Server State and Data Fetching
Modern React apps often need to manage server state—data fetched from APIs that must be cached, synchronized, and updated. Libraries like React Query, SWR, and Relay help manage server state, caching, background updates, and optimistic UI.
Example with React Query:
import { useQuery } from '@tanstack/react-query'
function Todos() {
const { data, isLoading } = useQuery(['todos'], fetchTodos)
if (isLoading) return <div>Loading...</div>
return (
<ul>
{data.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
)
}
URL State
URL state includes data stored in the URL, such as query parameters, route params, and hash fragments. This is useful for filters, pagination, and sharing links. Use libraries like React Router or Next.js router to manage URL state.
Conclusion: Choosing the Right State Management Approach
The best state management approach depends on your app's complexity and your team's needs:
- For simple apps,
useState
anduseReducer
may suffice. - For global or complex state, consider Context, Zustand, MobX, or atomic libraries like Recoil and Jotai.
- For server state, use React Query, SWR, or Relay.
- For URL state, leverage React Router or Next.js router.
Evaluate your application's needs, the scale of state changes, and future growth potential. As React evolves, new state management tools and patterns emerge. Atomic state management is gaining traction for modularity and performance. Ultimately, choose the approach that makes your code more readable, maintainable, and scalable.
Managing state between components is fundamental in React. By understanding the available methods and tools, you can build robust, efficient applications. Stay informed about the latest best practices to maximize React's capabilities.