Dev.to WebDev 🛠 Dev 👁 0 📖 8 min read

React Mastery Series – Day 32: State Management Architecture – Redux Toolkit vs Context API vs TanStack Query

Welcome back to the React Mastery Series! In Day 31, we explored Advanced React Architecture and learned how to design large-scale applications using: Absolutely — let's continue with Day 32, focusing on one of the mos

Welcome back to the React Mastery Series!

In Day 31, we explored Advanced React Architecture and learned how to design large-scale applications using:

Absolutely — let's continue with Day 32, focusing on one of the most important architectural decisions in a production React application.

  • Feature-based architecture
  • Clean Architecture principles
  • Domain boundaries
  • Separation of concerns
  • Dependency direction
  • Modular frontend design

Today, we're going to solve a question that comes up in almost every React architecture discussion:

Where should application state live?

Should we use:

  • useState?
  • Context API?
  • Redux Toolkit?
  • TanStack Query?
  • Something else?

The answer is:

It depends on what kind of state you're managing.

Understanding this distinction is much more important than simply knowing how to use a state-management library.

What Is State?

State is information that can change over time and affect what the application displays or does.

Examples:

User logged in
Selected account
Shopping cart
Modal opened
Theme selected
API response
Loading status
Search text

But not all state is the same.

That's the key architectural insight.

Two Major Categories of State

A useful way to think about application state is:

Application State
       │
       ├── Client State
       └── Server State

Let's understand the difference.

Client State

Client state is primarily owned and controlled by the frontend.

Examples:

  • Modal visibility
  • Selected tab
  • Form input
  • Theme
  • Sidebar state
  • Local UI preferences

Example:

const [isOpen, setIsOpen] = useState(false);

The server doesn't care whether your modal is open.

Therefore, this is client state.

Server State

Server state comes from a backend system.

Examples:

  • User profile
  • Account balances
  • Transactions
  • Products
  • Orders
  • Notifications

For example:

React Application
       │
       ▼
GET /accounts
       │
       ▼
Backend
       │
       ▼
Account Data

The backend owns this data.

The frontend is essentially consuming and caching it.

This is server state.

Why This Distinction Matters

A common mistake is putting everything into Redux.

For example:

API Response
      ↓
Redux
      ↓
Component

This can work.

But server state has unique requirements:

  • Caching
  • Refetching
  • Deduplication
  • Background synchronization
  • Retry handling
  • Pagination
  • Stale data management

Redux itself doesn't automatically solve all of these concerns.

That's where tools such as TanStack Query become useful.

State Management Decision Tree

A practical decision framework:

Do I need state?
      │
      ▼
Only one component?
      │
     Yes
      │
      ▼
   useState

If multiple components need it:

      │
      ▼
Is it simple shared state?
      │
     Yes
      │
      ▼
 Context API

If it's complex global client state:

      │
      ▼
 Redux Toolkit

If the data comes from a server:

      │
      ▼
 TanStack Query

This isn't an absolute rule, but it's a useful starting point.

1. useState

Start with the simplest solution.

Example:

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount((value) => value + 1)}>
      {count}
    </button>
  );
}

If only one component needs the state, don't introduce a global state library.

2. useReducer

When local state becomes more complex, useReducer can help.

Example:

type State = {
  count: number;
};

type Action =
  | { type: "increment" }
  | { type: "decrement" };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "increment":
      return {
        count: state.count + 1,
      };

    case "decrement":
      return {
        count: state.count - 1,
      };

    default:
      return state;
  }
}

This is useful when state transitions become more structured.

3. Context API

Context allows values to be shared across a component tree without passing props through every level.

Example:

const ThemeContext = createContext("light");

Provider:

<ThemeContext.Provider value="dark">
  <App />
</ThemeContext.Provider>

Consumer:

const theme = useContext(ThemeContext);

Good Context API Use Cases

Context works well for relatively stable cross-cutting concerns such as:

Theme
Authentication Context
Localization
Feature Configuration

Example:

<AuthProvider>
  <App />
</AuthProvider>

When Context Can Become a Problem

Consider:

Global Context
      │
      ├── User
      ├── Notifications
      ├── Accounts
      ├── Transactions
      ├── Preferences
      └── Payments

Now many unrelated values live inside the same context.

A change to one value can cause consumers to re-render unnecessarily.

The solution isn't necessarily "never use Context."

Instead:

Keep contexts focused and avoid turning Context into a global dumping ground.

4. Redux Toolkit

Redux Toolkit is a powerful choice for complex client-side state.

Example:

features
├── auth
│   └── authSlice.ts
├── cart
│   └── cartSlice.ts
└── preferences
    └── preferencesSlice.ts

A slice:

import { createSlice, PayloadAction } from "@reduxjs/toolkit";

type CartState = {
  items: string[];
};

const initialState: CartState = {
  items: [],
};

const cartSlice = createSlice({
  name: "cart",
  initialState,
  reducers: {
    addItem(
      state,
      action: PayloadAction<string>
    ) {
      state.items.push(action.payload);
    },

    removeItem(
      state,
      action: PayloadAction<string>
    ) {
      state.items =
        state.items.filter(
          (item) => item !== action.payload
        );
    },
  },
});

export const {
  addItem,
  removeItem,
} = cartSlice.actions;

export default cartSlice.reducer;

Redux Toolkit simplifies traditional Redux significantly.

When Redux Makes Sense

Redux Toolkit can be valuable when you have:

  • Complex shared state
  • Multiple state transitions
  • Cross-feature workflows
  • Centralized client-side state
  • Strong debugging requirements
  • Middleware requirements
  • Large development teams

For example:

Payment Workflow
↓
Selected Account
↓
Selected Beneficiary
↓
Payment Amount
↓
Validation
↓
Confirmation
↓
Submission Status

If many unrelated components participate in this workflow, centralized state can be useful.

5. TanStack Query

Now we come to an important distinction.

Suppose your application needs:

GET /accounts

The response needs to be:

  • Cached
  • Refetched
  • Shared between components
  • Invalidated after mutations
  • Retried when appropriate

This is a server-state problem.

TanStack Query is designed specifically for this kind of workflow.

Example:

import { useQuery } from "@tanstack/react-query";

function Accounts() {
  const { data, isPending, isError } = useQuery({
    queryKey: ["accounts"],
    queryFn: getAccounts,
  });

  if (isPending) {
    return <p>Loading...</p>;
  }

  if (isError) {
    return <p>Unable to load accounts.</p>;
  }

  return (
    <ul>
      {data.map((account) => (
        <li key={account.id}>
          {account.name}
        </li>
      ))}
    </ul>
  );
}

The library manages much of the server-state lifecycle for you.

Server State Lifecycle

A server-state library can manage concepts such as:

Request
   ↓
Loading
   ↓
Success
   ↓
Cache
   ↓
Stale
   ↓
Refetch
   ↓
Updated Cache

This is considerably more than simply storing an API response in Redux.

Mutations

Reading data is only half the story.

Applications also modify server data.

Example:

const mutation = useMutation({ mutationFn: createPayment });

Then:

await mutation.mutateAsync(payment);

After a successful mutation, related queries can be invalidated so the UI gets fresh server data.

Redux vs TanStack Query

A useful mental model:

Requirement Redux Toolkit TanStack Query
Client state ✅ ❌
Server state Possible ✅
Caching API data Manual Built-in
Refetching Manual Built-in
Global workflows ✅ Limited
Complex state transitions ✅ ❌
Background synchronization Manual ✅

The libraries solve different problems.

Can We Use Both?

Absolutely.

A production application can use:

React
 │
 ├── useState
 ├── Context
 ├── Redux Toolkit
 └── TanStack Query

For example:

Authentication
      ↓
Redux Toolkit

Theme
      ↓
Context API

Transaction API Data
      ↓
TanStack Query

Modal State
      ↓
useState

This is often more maintainable than forcing everything into one solution.

Example Banking Application

Imagine a digital banking application.

Local UI State

Selected tab
Modal visibility
Form input

Use:

useState

Authentication

Current user
Authentication status
Session information

Could use:

Context

or a centralized client-state solution depending on the application's requirements.

Account Data

Account balances
Transactions
Statements

Use:

TanStack Query

because this is server-owned data.

Complex Payment Workflow

Account
Beneficiary
Amount
Validation
Confirmation
Submission

Could use:

Redux Toolkit

if the workflow requires complex cross-component client state.

Avoid Duplicate Sources of Truth

One of the biggest architecture problems is storing the same data in multiple places.

For example:

Backend
   ↓
TanStack Query
   ↓
Redux
   ↓
Component State

Now you have multiple copies of the same information.

Which one is correct?

This creates synchronization problems.

Prefer a single source of truth for each category of state.

State Ownership

A powerful question to ask is:

Who owns this state?

For example:

Modal Open?
→ Component

Theme?
→ Context

Account Balance?
→ Server

Payment Draft?
→ Application State

Shopping Cart?
→ Global Client State

Once ownership is clear, choosing the state-management solution becomes much easier.

State Colocation

Another important principle is:

Keep state as close as possible to where it is used.

Bad:

Entire Application
        ↓
Global State
        ↓
Modal Visibility

Better:

function PaymentPage() {
  const [isModalOpen, setIsModalOpen] =
    useState(false);

  // ...
}

Don't make local state global without a reason.

State Normalization

Large applications may have deeply nested data.

Example:

{
  "users": [
    {
      "id": 1,
      "accounts": [
        {
          "id": 101
        }
      ]
    }
  ]
}

Normalized state can reduce duplication.

Conceptually:

users
├── 1
└── 2

accounts
├── 101
└── 102

Normalization becomes especially useful when entities are shared across many parts of the application.

Selectors

With Redux, selectors allow components to read only the state they need.

Example:

const selectCartItems = (
  state: RootState
) => state.cart.items;

Component:

const items = useSelector(selectCartItems);

Selectors can help encapsulate state access and reduce unnecessary coupling to the store structure.

Performance Considerations

State architecture also affects rendering performance.

Imagine:

Global State Changes

↓

100 Components Subscribe

↓

Many Components Re-render

Instead, design state boundaries carefully.

Use:

  • Focused contexts
  • Redux selectors
  • Local state
  • Memoization when justified
  • Server-state caching

Performance should be measured rather than optimized blindly.

A Practical Decision Matrix

When deciding where state belongs:

Question Preferred Option
Only one component needs it? useState
Complex local transitions? useReducer
Shared stable configuration? Context
Complex global client state? Redux Toolkit
Backend/server-owned data? TanStack Query
Temporary UI state? Local state

This isn't a strict law.

It's a starting point for architectural decisions.

Recommended Enterprise Architecture

A mature React application might look like:

                    React App
                       │
       ┌───────────────┼────────────────┐
       │               │                │
   Local State       Context          Redux
       │               │                │
   useState       Theme/Auth       Client State
       │
       │
       └───────────────┐
                       │
                TanStack Query
                       │
                       ▼
                 Backend APIs

Each tool has a clearly defined responsibility.

Common Mistakes

1. "Everything Goes Into Redux"

This often creates unnecessary complexity.

2. Using Context for Everything

Context is not automatically a replacement for Redux or a server-state library.

3. Putting API Data Into Multiple Stores

Avoid maintaining duplicate copies of server data unless there is a strong architectural reason.

4. Making Local State Global

A state value being shared by two components doesn't automatically mean it belongs in a global store.

5. Choosing a Library Before Understanding the Problem

Don't start with:

"Should we use Redux?"

Start with:

"What kind of state are we managing?"

That's the architectural question.

Senior Frontend Engineer Mindset

A junior developer asks:

"Which state library should I use?"

A senior developer asks:

"What type of state is this?"

An architect asks:

"Who owns this state, who consumes it, how long does it live, and what consistency guarantees do we need?"

That difference in thinking is what makes state architecture scalable.

Key Takeaways

Today, we learned:

✅ useState is ideal for local component state.
✅ useReducer helps manage complex local state transitions.
✅ Context works well for focused cross-cutting concerns.
✅ Redux Toolkit is useful for complex global client state.
✅ TanStack Query is designed for server-state management.
✅ Avoid multiple sources of truth.
✅ Keep state as close as possible to where it is used.
✅ Choose the state-management solution based on the problem—not popularity.

Coming Next 🚀

In Day 33, we'll explore another critical area of frontend architecture:

React API Architecture – Axios, Fetch, Service Layers, Interceptors & Error Handling

We'll build a production-ready API architecture covering:

  • API client configuration
  • Axios vs Fetch
  • Service layers
  • Request interceptors
  • Response interceptors
  • Authentication headers
  • Token refresh
  • Centralized error handling
  • Request cancellation
  • Retry strategies
  • API types with TypeScript
  • Handling multiple backend services

This will bring together many concepts we've already learned and show how they fit into a real enterprise React application.

Happy Coding! 🚀

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.