State Management Patterns in React: When to Reach for What
State management is one of those React topics that starts simple and becomes complicated surprisingly quickly.
At the beginning, you might only need:
const [count, setCount] = useState(0);
Then the application grows.
Suddenly you have:
Authentication state
Shopping cart state
Form state
Modal state
Filters
URL parameters
Server data
Cached API responses
User preferences
Notifications
Feature flags
Shared state between components
At that point, developers often ask:
Should I use Context?
Should I add Redux?
Is Zustand better?
Should this data be stored globally?
Should API data be in Redux?
Can I just use
useState?
The important thing is that not all state is the same.
The best React state management architecture usually comes from first identifying what kind of state you have, where it belongs, and who needs to access it.
This guide explains the major React state management patterns, when to use each one, and how to avoid adding unnecessary complexity.
What Is State in React?
State is data that can change over time and influence what a component renders or how it behaves.
For example:
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
Here:
count
is state.
When the state changes, React can re-render the component so the UI reflects the new value.
But a modern application can contain several different categories of state.
The Four Major Types of Application State
Before choosing a state management library, classify the state.
A useful model is:
Application State
│
├── Local UI State
├── Shared Client State
├── Server State
└── URL / Navigation State
These categories have different characteristics.
1. Local UI State
Examples:
Modal open/closed
Selected tab
Input value
Accordion state
Temporary form state
Typical tools:
useState
useReducer
2. Shared Client State
Examples:
Authentication information
Shopping cart
Theme
User preferences
Application settings
Possible tools:
Context
Redux
Zustand
Other state libraries
3. Server State
Examples:
Users from an API
Products
Orders
Notifications
Dashboard data
This state comes from a remote source and usually has:
Loading state
Error state
Cache
Refetching
Stale data
Pagination
Synchronization
Tools such as TanStack Query can be useful here.
4. URL State
Examples:
/products?category=mobile&sort=price&page=2
The URL itself is state.
Typical examples include:
Search queries
Filters
Pagination
Sorting
Selected tabs
Route parameters
URL state is particularly useful when the state should be:
Shareable
Bookmarkable
Browser-navigation friendly
The Most Important Rule
Before adding a state management library, ask:
Who needs this state?
If only one component needs it, keep it local.
If a small component tree needs it, consider lifting state or using Context.
If many unrelated parts of the application need it, consider a client-state store.
If the state comes from an API, consider a server-state solution.
If the state belongs in the URL, keep it in the URL.
This simple classification prevents many unnecessary state-management problems.
1. useState: The Default Starting Point
useState should usually be the first tool you consider.
Example:
function Profile() {
const [isEditing, setIsEditing] = useState(false);
return (
<>
<button onClick={() => setIsEditing(!isEditing)}>
{isEditing ? "Cancel" : "Edit"}
</button>
</>
);
}
This is local UI state.
There is no reason to introduce Redux or another global state library for this.
Good use cases for useState
Use useState for:
Input values
Modal visibility
Tabs
Dropdowns
Toggle buttons
Local loading indicators
Temporary UI state
Small component-level data
For example:
const [search, setSearch] = useState("");
const [isOpen, setIsOpen] = useState(false);
const [selectedTab, setSelectedTab] = useState("overview");
This is simple and easy to maintain.
When useState Starts Becoming Difficult
Consider a component with:
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [phone, setPhone] = useState("");
const [address, setAddress] = useState("");
const [city, setCity] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
As the state becomes more interconnected, managing individual setters can become harder to reason about.
That's where useReducer can become useful.
2. useReducer: Complex Local State
useReducer is useful when state transitions are more complex or several pieces of state change together.
Example:
const initialState = {
status: "idle",
data: null,
error: null
};
function reducer(state, action) {
switch (action.type) {
case "FETCH_START":
return {
...state,
status: "loading",
error: null
};
case "FETCH_SUCCESS":
return {
status: "success",
data: action.payload,
error: null
};
case "FETCH_ERROR":
return {
status: "error",
data: null,
error: action.payload
};
default:
return state;
}
}
Then:
const [state, dispatch] = useReducer(
reducer,
initialState
);
Instead of multiple setters:
setLoading(true);
setError(null);
setData(null);
you can describe the event:
dispatch({
type: "FETCH_START"
});
useState vs useReducer
Situation | useState | useReducer |
|---|---|---|
Simple value | Excellent fit | Usually unnecessary |
Boolean toggle | Excellent fit | Usually unnecessary |
Small form | Good | Sometimes |
Complex form | Possible | Often useful |
Many related transitions | Can become messy | Good fit |
State machine-like logic | Less suitable | Better fit |
Shared global state | Not directly | Not directly |
The important distinction is:
useReducerorganizes complex state transitions; it does not automatically make state global.
3. Lifting State Up
Sometimes you don't need Context or Redux.
You simply need to move state to the nearest common parent.
Suppose:
Parent
├── SearchInput
└── ProductList
Both components need access to the search value.
Keep the state in the parent:
function ProductsPage() {
const [search, setSearch] = useState("");
return (
<>
<SearchInput
value={search}
onChange={setSearch}
/>
<ProductList search={search} />
</>
);
}
This pattern is called lifting state up.
It is one of the simplest and most useful React state-management patterns.
When Lifting State Up Becomes Painful
Imagine:
App
└── Dashboard
└── Layout
└── Sidebar
└── UserMenu
If a value needs to travel through several components:
<App user={user}>
<Dashboard user={user}>
<Layout user={user}>
<Sidebar user={user}>
<UserMenu user={user} />
</Sidebar>
</Layout>
</Dashboard>
</App>
This can become prop drilling.
Context can help when many components need the same value.
4. Context API: Sharing Values Through a Tree
React Context allows components to access a value without explicitly passing it through every intermediate component.
For example:
const ThemeContext = createContext(null);
Provider:
<ThemeContext.Provider value={theme}>
<App />
</ThemeContext.Provider>
Consumer:
const theme = useContext(ThemeContext);
Modern React also provides the ability to consume context using the use API in supported React versions and contexts, but useContext remains the familiar pattern for many existing applications.
Good Context Use Cases
Context works well for values such as:
Theme
Locale
Authentication information
User preferences
Feature configuration
Dependency-like application services
For example:
<ThemeProvider>
<AuthProvider>
<App />
</AuthProvider>
</ThemeProvider>
Context Is Not Automatically a Global State Manager
This is an important distinction.
Context answers:
"How can this value be made available to components?"
It does not provide all the features of a dedicated state-management library.
You can combine Context with state:
const AuthContext = createContext(null);
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
return (
<AuthContext.Provider value={{ user, setUser }}>
{children}
</AuthContext.Provider>
);
}
Now Context distributes the state.
But as the application grows, you may need more sophisticated patterns for:
selective subscriptions
complex updates
debugging
middleware
persistence
derived state
large-scale organization
5. Redux: Structured Global Client State
Redux is a predictable state container built around centralized state and explicit actions/reducers.
Modern Redux applications generally use Redux Toolkit rather than writing Redux from scratch.
A simplified Redux architecture looks like:
Component
↓
dispatch(action)
↓
Reducer
↓
Store
↓
Updated State
↓
Component
For example:
dispatch(
cartActions.addItem(product)
);
The reducer updates the state.
When Redux Makes Sense
Redux can be useful when you have:
Large application state
Many unrelated consumers
Complex state transitions
Multiple teams
Extensive debugging requirements
Middleware requirements
Existing Redux infrastructure
A need for explicit state-update patterns
For large applications, consistency can be more valuable than minimizing the number of dependencies.
Redux Is Not Only for Large Applications
The opposite extreme is also worth avoiding.
Redux can work in smaller applications, but introducing it only because:
"Real applications use Redux"
is not a good architectural reason.
Choose it when its capabilities solve a real problem.
6. Zustand: Lightweight Client State
Zustand is another popular option for client-side state.
A basic store can look like:
import { create } from "zustand";
const useCartStore = create((set) => ({
items: [],
addItem: (item) =>
set((state) => ({
items: [...state.items, item]
})),
clearCart: () =>
set({
items: []
})
}));
A component can select the state it needs:
const items = useCartStore(
(state) => state.items
);
This approach can be attractive when you want:
Simple global state
Small API surface
Less boilerplate
Store-based architecture
Selective subscriptions
Zustand is not a replacement for every other state solution. It primarily addresses client-side state management.
Redux vs Zustand
The choice is less about "which library is better" and more about application requirements.
Area | Redux Toolkit | Zustand |
|---|---|---|
Structured architecture | Strong | Flexible |
Boilerplate | Moderate | Low |
Large-team conventions | Strong | More team-dependent |
Middleware ecosystem | Mature | Available |
DevTools | Strong | Supported |
Simple stores | More setup | Very simple |
Complex enterprise state | Often suitable | Can be suitable |
Learning curve | Moderate | Generally smaller |
Both can be valid choices.
7. Server State Is Different
One of the biggest state-management mistakes is treating API data exactly like local client state.
Suppose your application loads:
/api/products
The response is not really owned by the React component.
It belongs to a remote server.
That means you may need:
Caching
Refetching
Retry
Stale data handling
Pagination
Background updates
Request deduplication
Loading states
Error states
This is commonly called server state.
TanStack Query and Server State
Libraries such as TanStack Query are designed around server-state problems.
Conceptually:
React Component
↓
TanStack Query
↓
Query Cache
↓
API
For example:
const {
data,
isPending,
error
} = useQuery({
queryKey: ["products"],
queryFn: fetchProducts
});
Instead of manually building:
loading
error
data
refetch
cache
retry
stale handling
the library manages many of these concerns.
This is one reason modern React applications often use a combination of:
Client State
+
Server State
rather than putting everything into Redux or another global store.
8. Don't Put Every API Response in Redux
Consider:
const [products, setProducts] = useState([]);
Then the team decides:
"Let's move all API data into Redux."
Now Redux contains:
products
users
orders
notifications
comments
reviews
But each of those datasets already belongs to the server.
You may now have to implement:
loading
error
cache invalidation
refetch
stale data
pagination
request deduplication
manually.
A server-state library may be more appropriate for this class of problem.
Redux can still be useful for client state or when the application intentionally uses Redux for server-state workflows.
9. URL State
URL parameters are an underrated form of state management.
Consider:
/products?search=iphone&sort=price&page=2
The URL contains:
search
sort
page
These values should often remain in the URL instead of being stored only in React state.
Why?
Because the URL is:
Shareable
Bookmarkable
Refresh-safe
Browser-history compatible
For example:
/products?category=mobile
can be shared with another user.
If category only exists inside:
const [category, setCategory] = useState("mobile");
then sharing the current application state becomes more difficult.
10. Form State
Forms are another special category.
A simple form might work perfectly with:
const [email, setEmail] = useState("");
But complex forms may contain:
Dozens of fields
Validation
Nested values
Dynamic fields
Submission states
Error messages
Dirty state
Touched state
In those cases, a dedicated form library may be appropriate.
The important point is that form state does not necessarily belong in your global application store.
A form is often temporary UI state.
11. Local Storage and Persistent State
Some state needs to survive a page refresh.
Examples:
theme
language
user preferences
dismissed banners
cart preferences
You might persist selected state to:
localStorage
sessionStorage
IndexedDB
For example:
localStorage.setItem(
"theme",
"dark"
);
But persistence should be deliberate.
Do not automatically persist everything.
Sensitive authentication data, tokens, and other private information require careful security design rather than blindly writing them to browser storage.
12. Derived State
Not every value needs to be stored.
Consider:
const [items, setItems] = useState([]);
const [total, setTotal] = useState(0);
If total can be calculated from items, storing both creates two sources of truth.
Instead:
const total = items.reduce(
(sum, item) => sum + item.price,
0
);
Now:
items → total
There is only one source of truth.
This is an important React principle:
Do not store state that can be reliably derived from existing state.
13. useMemo Is Not a State Manager
A common misunderstanding is:
const total = useMemo(() => {
return calculateTotal(items);
}, [items]);
useMemo can memoize a calculation.
It does not replace state management.
Think of these tools differently:
useState
→ stores state
useReducer
→ manages complex state transitions
Context
→ distributes values through a component tree
useMemo
→ memoizes a calculation
useCallback
→ memoizes a function reference
Do not introduce useMemo simply because the application has "too much state."
14. State Colocation
One of the most useful patterns in React is state colocation.
Keep state as close as possible to the components that actually use it.
Instead of:
Global Store
├── Modal State
├── Search Input
├── Dropdown
├── Product Filter
├── Tooltip
└── Button State
prefer:
ProductPage
├── ProductFilter state
├── ProductList
└── ProductDetails
Local state is easier to understand when it remains local.
Global state should be reserved for state that genuinely needs global access.
15. Avoid Global State for Temporary UI
Imagine a modal:
const [isOpen, setIsOpen] = useState(false);
There is usually no reason to put this into Redux:
dispatch(
modalActions.open("delete-user")
);
unless the modal system genuinely needs global orchestration.
Simple state should remain simple.
16. State Machines for Complex Workflows
Some UI flows are more complicated than:
true / false
Consider a payment process:
idle
↓
processing
↓
success
or:
idle
↓
processing
↓
error
↓
retrying
↓
processing
A state machine approach can make these transitions explicit.
Instead of allowing arbitrary combinations:
isLoading = true
isSuccess = true
hasError = true
you can define valid states:
idle
processing
success
error
This reduces impossible state combinations.
State machines can be useful for:
Checkout
Authentication
Multi-step forms
Upload workflows
Media players
Complex dialogs
17. React State Management Architecture
A practical modern architecture can look like this:
React Application
|
+------------------+------------------+
| | |
Local UI State Client State Server State
| | |
useState Context Query Library
useReducer Redux API Cache
Zustand
|
URL State
|
Router / URL
The important idea is that different problems can use different tools.
You do not need one library for everything.
18. A Practical Decision Tree
When you need to store a new value, ask these questions.
Step 1: Is it derived?
If yes:
Calculate it.
Don't store it separately.
Step 2: Is it only used by one component?
Use:
useState
Step 3: Is the local state transition complex?
Consider:
useReducer
Step 4: Do nearby components need it?
Consider:
Lift state up
Step 5: Do many components across the tree need it?
Consider:
Context
or a client-state library.
Step 6: Is it complex global client state?
Consider:
Redux Toolkit
Zustand
or another appropriate store.
Step 7: Does it come from a server?
Consider:
TanStack Query
or the data-fetching/cache solution provided by your framework or API architecture.
Step 8: Should users be able to share/bookmark it?
Consider:
URL state
19. A Real-World E-Commerce Example
Imagine you are building an e-commerce application.
You might have:
Local state
Product image carousel
Modal visibility
Dropdown state
Use:
useState
Complex local state
Checkout workflow
Use:
useReducer
Global client state
Shopping cart
Theme
User preferences
Use:
Context / Redux / Zustand
depending on complexity.
Server state
Products
Orders
Reviews
Recommendations
Use:
TanStack Query
or an equivalent server-state/data-fetching solution.
URL state
Search
Filters
Sorting
Pagination
Use:
URL parameters
This architecture is often cleaner than putting everything into a single global store.
20. A Real-World React Native Example
The same principles apply to React Native.
Suppose you are building a food delivery application.
Local UI state
Selected tab
Modal
Accordion
Temporary input
Use:
useState
Shared client state
Cart
User preferences
Authentication state
Use:
Context
Redux Toolkit
Zustand
depending on requirements.
Server state
Restaurants
Menu
Orders
Delivery status
Use a server-state/data-fetching solution.
Persistent state
Theme
Preferences
Selected delivery address
Use appropriate persistent storage.
Again, the key is to classify the state first.
21. Common State Management Mistakes
Mistake 1: Making Everything Global
Global state is not automatically better.
The more state you make global, the harder it can become to understand ownership and update flows.
Mistake 2: Putting Server State in Client State
API data has different lifecycle requirements.
Treating it exactly like local UI state often creates unnecessary code.
Mistake 3: Using Context for Everything
Context can be excellent for shared values, but a rapidly changing, very large context value can cause unnecessary re-rendering of consumers depending on how the provider and consumers are structured.
Split contexts or use a store with selective subscriptions when appropriate.
Mistake 4: Duplicating State
Avoid:
items
total
itemCount
when:
total
itemCount
can be derived from items.
Mistake 5: Storing URL State Separately
If a filter belongs in the URL, don't unnecessarily maintain two independent sources of truth:
URL filter
+
React filter
Synchronizing both can introduce bugs.
Mistake 6: Choosing a Library Before Understanding the Problem
Don't start with:
"Should we use Redux or Zustand?"
Start with:
"What type of state are we managing?"
That question usually makes the library decision easier.
22. Performance Considerations
State management can affect rendering performance.
Suppose a global state update causes many components to re-render.
The solution isn't always:
useMemo(...)
useCallback(...)
React.memo(...)
First investigate:
What state changed?
Who subscribes to it?
Which components actually need it?
Can the state be colocated?
Can subscriptions be more granular?
Is derived data being recalculated unnecessarily?
Good state architecture often improves performance before optimization hooks are needed.
23. React State Management Best Practices
1. Keep state local by default
Don't globalize state without a reason.
2. Separate server state from client state
Remote API data has different lifecycle requirements.
3. Avoid duplicated state
Prefer one source of truth.
4. Use derived values
Calculate values that can be derived.
5. Use URL state intentionally
Search, filters, and pagination often belong in the URL.
6. Choose tools based on state type
Don't choose a library first.
7. Keep providers focused
Avoid one massive Context containing the entire application.
8. Keep reducers predictable
Reducers should describe state transitions clearly.
9. Don't optimize prematurely
Measure rendering and state-update behavior before adding memoization.
10. Document global state
For large applications, developers should understand:
Who owns the state?
Who can update it?
Where is it persisted?
Is it client state or server state?
24. Quick React State Management Cheat Sheet
State Type | Recommended Starting Point |
|---|---|
Boolean toggle |
|
Input value |
|
Local component data |
|
Complex local workflow |
|
Shared parent/child state | Lift state up |
Theme | Context |
Locale | Context |
Authentication context | Context or client store |
Complex global client state | Redux Toolkit / Zustand |
API/server data | Server-state/query library |
Search/filter state | URL |
Pagination | URL |
Derived value | Calculate it |
Complex workflow | Reducer/state machine |
Final Takeaway
React state management does not need to start with a library.
Start by identifying the state.
Is it derived?
↓
Calculate it
Is it local?
↓
useState
Is the transition complex?
↓
useReducer
Is it shared through a tree?
↓
Context
Is it complex global client state?
↓
Redux / Zustand / another store
Does it come from the server?
↓
Server-state solution
Does it belong in the URL?
↓
URL state
The biggest state-management improvement is often not choosing a better library.
It is putting each type of state in the right place.
A well-structured React application may use several patterns at the same time:
useState
+
useReducer
+
Context
+
Client Store
+
Server-State Cache
+
URL State
That is not a problem.
In fact, separating these responsibilities can make an application easier to understand, test, maintain, and scale.
The goal of React state management is not to put everything into one store.
The goal is to make state ownership, updates, and data flow predictable.
Frequently Asked Questions
What is the best state management in React?
There is no single best solution for every React application. Start with local React state and introduce Context, a client-state library, or a server-state solution when the application's requirements justify it.
Should I use Redux in 2026?
Redux can still be appropriate for applications that benefit from centralized, structured client-state management, predictable updates, middleware, debugging, and established team conventions. It should be selected based on requirements rather than simply because it is popular.
Is Zustand better than Redux?
They solve similar client-state problems with different APIs and architectural trade-offs. Zustand generally emphasizes a lightweight store API, while Redux Toolkit provides a more structured ecosystem and conventions.
Should API data be stored in Redux?
It can be, but API data is server state and often benefits from specialized caching, refetching, synchronization, and stale-data handling. A server-state/query library can be a better fit for those requirements.
Is Context API a replacement for Redux?
Not necessarily. Context is primarily a mechanism for making values available through a component tree. It can be combined with React state, reducers, or other approaches, but it does not provide every feature of a dedicated global state library.
Should every React application use global state?
No. Many applications can be built effectively with local state, lifted state, Context, URL state, and a server-state solution.
When should I use useReducer instead of useState?
Use useReducer when state transitions become complex, multiple values change together, or you want explicit action-driven state transitions. For simple values and UI toggles, useState is usually simpler.
What is server state in React?
Server state is data owned by a remote backend, such as users, products, orders, or notifications. It often requires caching, refetching, synchronization, pagination, and stale-data handling.
Should search filters be stored in React state?
If filters need to be shareable, bookmarkable, or preserved through browser navigation, URL state is often a better fit. Local React state can still be appropriate for temporary UI-only filters.



