Skip to main content
DailyKiUpdate
Technology

State Management Patterns in React: When to Reach for What

Learn React state management patterns and when to use useState, useReducer, Context API, Redux, Zustand, server state libraries, URL state, and local storage.

By Rajendra KumarUpdated November 3, 202619 min read139 views
State Management Patterns in React: When to Reach for What

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:

useReducer organizes 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

useState

Input value

useState

Local component data

useState

Complex local workflow

useReducer

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.

Share this article

Comments