DEV Community

Cover image for Redux Explained: Concepts, Architecture, Examples, and Real-World Patterns
Abanoub Kerols
Abanoub Kerols

Posted on

Redux Explained: Concepts, Architecture, Examples, and Real-World Patterns

Redux is more than a state management library. It is a predictable architecture for managing application state through explicit actions, pure state transitions, and a centralized store.

If you have worked with React, Angular, or modern frontend applications, you have probably encountered problems such as:

  • Multiple components needing the same state
  • State being passed through many layers of components
  • Difficult-to-debug state changes
  • Complex asynchronous operations
  • Inconsistent application state
  • Business logic scattered across components

Redux was designed to solve these problems by introducing a predictable and structured approach to state management.

In this article, we will go from the fundamentals to practical Redux development, including:

  • What Redux is
  • Why Redux exists
  • Core Redux concepts
  • Store
  • Actions
  • Reducers
  • Dispatch
  • Selectors
  • Immutability
  • Redux data flow
  • Middleware
  • Async operations
  • Redux Toolkit
  • RTK Query
  • Entity management
  • Real-world architecture
  • Common mistakes
  • A complete example

1. What Is Redux?

Redux is a predictable state management library.

The basic idea is simple:

UI
 ↓
Dispatch Action
 ↓
Reducer
 ↓
New State
 ↓
Store
 ↓
UI Updates
Enter fullscreen mode Exit fullscreen mode

Instead of allowing components to modify application state however they want, Redux creates a controlled flow.

For example:

dispatch({
  type: "counter/increment"
});
Enter fullscreen mode Exit fullscreen mode

The action reaches a reducer:

function counterReducer(state, action) {
  if (action.type === "counter/increment") {
    return {
      ...state,
      value: state.value + 1
    };
  }

  return state;
}
Enter fullscreen mode Exit fullscreen mode

The reducer produces the next state.


2. Why Do We Need Redux?

Consider a large application.

You may have:

App
 ├── Header
 │    └── UserMenu
 │         └── UserProfile
 │
 ├── Dashboard
 │    ├── Statistics
 │    ├── Orders
 │    └── Notifications
 │
 └── Sidebar
Enter fullscreen mode Exit fullscreen mode

Suppose the logged-in user is needed by:

  • Header
  • UserMenu
  • Dashboard
  • Sidebar
  • Notifications

Without centralized state management, you may end up passing:

App
 ↓
Header
 ↓
UserMenu
 ↓
UserProfile
Enter fullscreen mode Exit fullscreen mode

This is commonly called prop drilling.

Redux provides a centralized store:

             Redux Store
          /       |       \
         ↓        ↓        ↓
      Header   Dashboard  Sidebar
Enter fullscreen mode Exit fullscreen mode

Components can subscribe to the state they need.


3. Redux Core Principles

Redux is based on several important principles.

3.1 Single Source of Truth

Application state is stored in one centralized store.

{
  user: {
    id: 1,
    name: "Abanoub"
  },
  cart: {
    items: []
  },
  products: [],
  ui: {
    theme: "dark"
  }
}
Enter fullscreen mode Exit fullscreen mode

Instead of having unrelated copies of important state throughout the application, Redux provides a central source.


4. State Is Read-Only

Components should not directly modify Redux state.

Incorrect:

state.counter.value++;
Enter fullscreen mode Exit fullscreen mode

Instead, dispatch an action:

dispatch({
  type: "counter/increment"
});
Enter fullscreen mode Exit fullscreen mode

The reducer determines how the state changes.


5. Changes Are Made Through Pure Functions

Reducers are responsible for calculating the next state.

Conceptually:

Previous State + Action = Next State
Enter fullscreen mode Exit fullscreen mode

Example:

const previousState = {
  value: 10
};

const action = {
  type: "increment"
};

const nextState = {
  value: 11
};
Enter fullscreen mode Exit fullscreen mode

The reducer:

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

    default:
      return state;
  }
}
Enter fullscreen mode Exit fullscreen mode

6. The Redux Store

The store contains the application state.

With modern Redux, the recommended approach is Redux Toolkit.

import { configureStore } from "@reduxjs/toolkit";

const store = configureStore({
  reducer: {
    counter: counterReducer
  }
});
Enter fullscreen mode Exit fullscreen mode

Conceptually:

Store
│
├── counter
├── user
├── products
├── cart
└── notifications
Enter fullscreen mode Exit fullscreen mode

7. Actions

An action describes what happened.

Example:

{
  type: "counter/increment"
}
Enter fullscreen mode Exit fullscreen mode

Another example:

{
  type: "cart/addItem",
  payload: {
    id: 10,
    name: "Keyboard"
  }
}
Enter fullscreen mode Exit fullscreen mode

The action does not directly modify state.

It describes an event.


8. Action Types

An action type is usually a string.

{
  type: "user/login"
}
Enter fullscreen mode Exit fullscreen mode

Examples:

user/login
user/logout
cart/addItem
cart/removeItem
products/load
products/delete
Enter fullscreen mode Exit fullscreen mode

A useful naming convention is:

feature/event
Enter fullscreen mode Exit fullscreen mode

For example:

cart/addItem
Enter fullscreen mode Exit fullscreen mode

9. Payload

The payload contains additional information.

{
  type: "cart/addItem",
  payload: {
    id: 1,
    name: "Laptop",
    price: 1200
  }
}
Enter fullscreen mode Exit fullscreen mode

Another example:

{
  type: "user/setUser",
  payload: {
    id: 5,
    name: "John"
  }
}
Enter fullscreen mode Exit fullscreen mode

10. Reducers

A reducer receives:

Current State
+
Action
Enter fullscreen mode Exit fullscreen mode

and returns:

Next State
Enter fullscreen mode Exit fullscreen mode

Example:

function counterReducer(state = { value: 0 }, action) {
  switch (action.type) {
    case "increment":
      return {
        ...state,
        value: state.value + 1
      };

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

    default:
      return state;
  }
}
Enter fullscreen mode Exit fullscreen mode

A reducer should be:

  • Predictable
  • Pure
  • Deterministic
  • Free of side effects

11. What Does "Pure Function" Mean?

A pure function:

  1. Produces the same output for the same input.
  2. Does not modify external state.
  3. Does not perform side effects.

Example:

function add(a, b) {
  return a + b;
}
Enter fullscreen mode Exit fullscreen mode

This is pure.

But:

let total = 0;

function add(value) {
  total += value;
}
Enter fullscreen mode Exit fullscreen mode

This is not pure because it modifies external state.

Reducers should follow the pure-function principle.


12. Dispatch

Dispatch sends an action to Redux.

dispatch({
  type: "counter/increment"
});
Enter fullscreen mode Exit fullscreen mode

The flow becomes:

Component
   ↓
dispatch(action)
   ↓
Redux
   ↓
Reducer
   ↓
New State
   ↓
Store
   ↓
Subscribed Components
Enter fullscreen mode Exit fullscreen mode

13. Selectors

Selectors read data from the Redux store.

For example:

const selectCount = state => state.counter.value;
Enter fullscreen mode Exit fullscreen mode

Then:

const count = useSelector(selectCount);
Enter fullscreen mode Exit fullscreen mode

Selectors help keep components independent from the exact shape of the state.

Instead of:

state.counter.value
Enter fullscreen mode Exit fullscreen mode

everywhere, you can use:

selectCount(state)
Enter fullscreen mode Exit fullscreen mode

14. Redux Data Flow

Redux follows a predictable one-way data flow.

┌─────────────┐
│     UI      │
└──────┬──────┘
       │
       │ dispatch()
       ↓
┌─────────────┐
│   Action    │
└──────┬──────┘
       ↓
┌─────────────┐
│   Reducer   │
└──────┬──────┘
       ↓
┌─────────────┐
│    Store    │
└──────┬──────┘
       ↓
┌─────────────┐
│     UI      │
└─────────────┘
Enter fullscreen mode Exit fullscreen mode

This predictable flow is one of Redux's biggest advantages.


15. A Simple Redux Example

Let's create a counter.

With Redux Toolkit:

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

const counterSlice = createSlice({
  name: "counter",

  initialState: {
    value: 0
  },

  reducers: {
    increment(state) {
      state.value += 1;
    },

    decrement(state) {
      state.value -= 1;
    },

    incrementByAmount(state, action) {
      state.value += action.payload;
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

Export the actions:

export const {
  increment,
  decrement,
  incrementByAmount
} = counterSlice.actions;
Enter fullscreen mode Exit fullscreen mode

Create the store:

const store = configureStore({
  reducer: {
    counter: counterSlice.reducer
  }
});
Enter fullscreen mode Exit fullscreen mode

Now:

store.dispatch(increment());
Enter fullscreen mode Exit fullscreen mode

Or:

store.dispatch(incrementByAmount(10));
Enter fullscreen mode Exit fullscreen mode

16. Why Does Redux Toolkit Allow Mutation?

You might notice:

state.value += 1;
Enter fullscreen mode Exit fullscreen mode

Earlier we said Redux state should not be mutated.

So why does this work?

Redux Toolkit uses Immer internally.

Immer allows you to write:

state.value += 1;
Enter fullscreen mode Exit fullscreen mode

while internally producing an immutable state update.

Conceptually:

Your Code
   ↓
Immer
   ↓
Immutable Update
   ↓
Redux State
Enter fullscreen mode Exit fullscreen mode

This gives developers simpler syntax while preserving Redux's immutability model.


17. createSlice

createSlice() is one of the most important Redux Toolkit APIs.

It combines:

  • State
  • Reducers
  • Action creators
  • Action types

Instead of manually writing:

const INCREMENT = "counter/increment";

function increment() {
  return {
    type: INCREMENT
  };
}

function reducer(state, action) {
  ...
}
Enter fullscreen mode Exit fullscreen mode

you can write:

const counterSlice = createSlice({
  name: "counter",

  initialState: {
    value: 0
  },

  reducers: {
    increment(state) {
      state.value++;
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

Redux Toolkit generates the action creator automatically.


18. Payload Actions

Suppose we want to add a product.

const cartSlice = createSlice({
  name: "cart",

  initialState: {
    items: []
  },

  reducers: {
    addItem(state, action) {
      state.items.push(action.payload);
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

Dispatch:

dispatch(
  addItem({
    id: 1,
    name: "Laptop",
    price: 1200
  })
);
Enter fullscreen mode Exit fullscreen mode

The action becomes conceptually:

{
  type: "cart/addItem",
  payload: {
    id: 1,
    name: "Laptop",
    price: 1200
  }
}
Enter fullscreen mode Exit fullscreen mode

19. Redux With React

Redux itself is independent of React.

To integrate Redux with React, we commonly use React-Redux.

First create the store:

const store = configureStore({
  reducer: {
    counter: counterReducer
  }
});
Enter fullscreen mode Exit fullscreen mode

Then provide it to React:

import { Provider } from "react-redux";

<Provider store={store}>
  <App />
</Provider>
Enter fullscreen mode Exit fullscreen mode

Now components can access Redux.


20. useSelector

useSelector() reads data.

import { useSelector } from "react-redux";

function Counter() {
  const count = useSelector(
    state => state.counter.value
  );

  return <h1>{count}</h1>;
}
Enter fullscreen mode Exit fullscreen mode

When the selected state changes, the component can re-render.


21. useDispatch

useDispatch() allows a component to dispatch actions.

import { useDispatch } from "react-redux";
import { increment } from "./counterSlice";

function CounterButton() {
  const dispatch = useDispatch();

  return (
    <button onClick={() => dispatch(increment())}>
      Increment
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

22. Complete React + Redux Example

function Counter() {
  const count = useSelector(
    state => state.counter.value
  );

  const dispatch = useDispatch();

  return (
    <div>
      <h1>{count}</h1>

      <button
        onClick={() => dispatch(increment())}
      >
        +
      </button>

      <button
        onClick={() => dispatch(decrement())}
      >
        -
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

The component does not directly change:

state.counter.value
Enter fullscreen mode Exit fullscreen mode

Instead:

Button
 ↓
dispatch(increment())
 ↓
Reducer
 ↓
New State
 ↓
useSelector
 ↓
Component Re-render
Enter fullscreen mode Exit fullscreen mode

23. Local State vs Redux State

Not every piece of state belongs in Redux.

For example:

const [isOpen, setIsOpen] = useState(false);
Enter fullscreen mode Exit fullscreen mode

This is usually local UI state.

Redux is more appropriate for state that needs to be shared or coordinated across different parts of an application.

Local State

Examples:

Modal open/closed
Input value
Dropdown state
Temporary UI state
Enter fullscreen mode Exit fullscreen mode

Global State

Examples:

Authenticated user
Shopping cart
Permissions
Global notifications
Shared application configuration
Cached server data
Enter fullscreen mode Exit fullscreen mode

24. Redux Is Not Always Necessary

A common mistake is:

"Every React application should use Redux."

Not true.

For a small application:

React
+
useState
+
useContext
Enter fullscreen mode Exit fullscreen mode

may be enough.

Redux becomes more valuable as state complexity increases.

A useful question is:

Is the complexity of shared state becoming harder to manage than the complexity of introducing Redux?


25. Middleware

Middleware sits between:

dispatch()
   ↓
Middleware
   ↓
Reducer
Enter fullscreen mode Exit fullscreen mode

It can:

  • Log actions
  • Perform asynchronous operations
  • Dispatch additional actions
  • Handle side effects
  • Integrate external services

Conceptually:

dispatch(action)
       ↓
   Middleware
       ↓
     Reducer
       ↓
      Store
Enter fullscreen mode Exit fullscreen mode

26. Why Do We Need Middleware?

Reducers should be pure.

Therefore, things like:

fetch()
Enter fullscreen mode Exit fullscreen mode

should not normally happen inside reducers.

Incorrect:

function reducer(state, action) {
  fetch("/api/products");

  return state;
}
Enter fullscreen mode Exit fullscreen mode

Instead, asynchronous work should happen outside reducers, commonly through middleware.


27. Redux Thunk

Redux Toolkit includes thunk middleware by default.

A thunk allows you to dispatch a function-like async workflow.

Example:

const fetchUsers = () => async dispatch => {
  dispatch(usersLoading());

  try {
    const response = await fetch("/api/users");

    const users = await response.json();

    dispatch(usersLoaded(users));
  } catch (error) {
    dispatch(usersFailed(error.message));
  }
};
Enter fullscreen mode Exit fullscreen mode

Then:

dispatch(fetchUsers());
Enter fullscreen mode Exit fullscreen mode

The flow becomes:

Component
   ↓
dispatch(fetchUsers())
   ↓
Thunk
   ↓
API Request
   ↓
dispatch(usersLoaded())
   ↓
Reducer
   ↓
Store
Enter fullscreen mode Exit fullscreen mode

28. createAsyncThunk

Redux Toolkit provides createAsyncThunk() to simplify common async workflows.

export const fetchUsers = createAsyncThunk(
  "users/fetchUsers",

  async () => {
    const response = await fetch("/api/users");

    return response.json();
  }
);
Enter fullscreen mode Exit fullscreen mode

Then handle the lifecycle:

const usersSlice = createSlice({
  name: "users",

  initialState: {
    data: [],
    loading: false,
    error: null
  },

  extraReducers: builder => {
    builder
      .addCase(fetchUsers.pending, state => {
        state.loading = true;
      })

      .addCase(fetchUsers.fulfilled, (state, action) => {
        state.loading = false;
        state.data = action.payload;
      })

      .addCase(fetchUsers.rejected, (state, action) => {
        state.loading = false;
        state.error = action.error.message;
      });
  }
});
Enter fullscreen mode Exit fullscreen mode

Now Redux automatically provides:

pending
fulfilled
rejected
Enter fullscreen mode Exit fullscreen mode

29. Handling API State

A common pattern is:

{
  data: [],
  loading: false,
  error: null
}
Enter fullscreen mode Exit fullscreen mode

The state transitions are:

Initial
 ↓
loading = true
 ↓
API Request
 ↓
 ┌───────────────┐
 │               │
Success        Failure
 │               │
 ↓               ↓
data          error
Enter fullscreen mode Exit fullscreen mode

This makes asynchronous state explicit.


30. RTK Query

For server data, Redux Toolkit provides RTK Query.

RTK Query is designed specifically for:

  • Fetching data
  • Caching
  • Refetching
  • Loading states
  • Error states
  • Request deduplication
  • Cache invalidation

Example:

const api = createApi({
  reducerPath: "api",

  baseQuery: fetchBaseQuery({
    baseUrl: "/api"
  }),

  endpoints: builder => ({
    getUsers: builder.query({
      query: () => "/users"
    })
  })
});
Enter fullscreen mode Exit fullscreen mode

Then:

const {
  data,
  isLoading,
  error
} = useGetUsersQuery();
Enter fullscreen mode Exit fullscreen mode

This removes a lot of manual async-state management.


31. Redux Toolkit vs Traditional Redux

Traditional Redux often required:

Action Types
Action Creators
Reducers
Store
Middleware
Selectors
Enter fullscreen mode Exit fullscreen mode

with a lot of boilerplate.

Modern Redux recommends Redux Toolkit.

Instead of:

const INCREMENT = "INCREMENT";

const increment = () => ({
  type: INCREMENT
});

function reducer(state, action) {
  switch (action.type) {
    ...
  }
}
Enter fullscreen mode Exit fullscreen mode

you can use:

const counterSlice = createSlice({
  name: "counter",

  initialState: {
    value: 0
  },

  reducers: {
    increment(state) {
      state.value++;
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

Redux Toolkit is now the recommended way to write Redux applications.


32. Normalizing State

Suppose you have:

{
  users: [
    {
      id: 1,
      name: "John"
    },
    {
      id: 2,
      name: "Sarah"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

As applications grow, searching and updating entities can become inefficient.

Normalized state can look like:

{
  users: {
    ids: [1, 2],

    entities: {
      1: {
        id: 1,
        name: "John"
      },

      2: {
        id: 2,
        name: "Sarah"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Redux Toolkit provides:

createEntityAdapter()
Enter fullscreen mode Exit fullscreen mode

for this use case.


33. createEntityAdapter

Example:

const usersAdapter = createEntityAdapter();

const initialState =
  usersAdapter.getInitialState();
Enter fullscreen mode Exit fullscreen mode

You can then use generated reducers and selectors to manage entities efficiently.

This is especially useful for:

Users
Products
Orders
Messages
Notifications
Enter fullscreen mode Exit fullscreen mode

34. Selectors in Large Applications

Instead of exposing state structure everywhere:

state.products.entities
Enter fullscreen mode Exit fullscreen mode

create selectors:

const selectProducts =
  state => state.products.entities;
Enter fullscreen mode Exit fullscreen mode

Then components use:

const products = useSelector(selectProducts);
Enter fullscreen mode Exit fullscreen mode

This improves maintainability.


35. Derived State

Sometimes you don't need to store everything.

For example, suppose Redux contains:

{
  products: [
    { price: 100 },
    { price: 200 },
    { price: 300 }
  ]
}
Enter fullscreen mode Exit fullscreen mode

You don't necessarily need:

{
  products: [...],
  totalPrice: 600
}
Enter fullscreen mode Exit fullscreen mode

You can derive it:

const selectTotalPrice = state =>
  state.products.reduce(
    (total, product) => total + product.price,
    0
  );
Enter fullscreen mode Exit fullscreen mode

This avoids duplicated state.


36. Memoized Selectors

For expensive calculations, selectors can be memoized.

Redux Toolkit works well with Reselect-style selectors.

Example:

const selectCompletedTodos = createSelector(
  [selectTodos],

  todos =>
    todos.filter(todo => todo.completed)
);
Enter fullscreen mode Exit fullscreen mode

The selector can avoid recalculating when its inputs haven't changed.


37. Redux Architecture

A scalable Redux application can be organized by features:

src/
│
├── app/
│   └── store.js
│
├── features/
│   ├── auth/
│   │   ├── authSlice.js
│   │   ├── authSelectors.js
│   │   └── authApi.js
│   │
│   ├── products/
│   │   ├── productsSlice.js
│   │   ├── productsSelectors.js
│   │   └── productsApi.js
│   │
│   └── cart/
│       ├── cartSlice.js
│       └── cartSelectors.js
│
└── components/
Enter fullscreen mode Exit fullscreen mode

This is called feature-based organization.

It scales much better than organizing everything by technical type.


38. Real-World Example: E-Commerce

Imagine an e-commerce application.

We might have:

Redux Store
│
├── auth
│
├── cart
│
├── products
│
├── orders
│
└── ui
Enter fullscreen mode Exit fullscreen mode

Example:

{
  auth: {
    user: null,
    token: null
  },

  cart: {
    items: []
  },

  products: {
    ids: [],
    entities: {}
  },

  orders: {
    ids: [],
    entities: {}
  },

  ui: {
    sidebarOpen: false
  }
}
Enter fullscreen mode Exit fullscreen mode

39. Adding a Product to the Cart

User clicks:

Add to Cart
Enter fullscreen mode Exit fullscreen mode

The component dispatches:

dispatch(
  addToCart({
    productId: 10,
    quantity: 1
  })
);
Enter fullscreen mode Exit fullscreen mode

Redux flow:

Product Component
       ↓
addToCart()
       ↓
Action
       ↓
Cart Reducer
       ↓
Cart State Updated
       ↓
Cart Icon
       ↓
UI Updated
Enter fullscreen mode Exit fullscreen mode

The cart icon can now select:

const itemCount = useSelector(
  selectCartItemCount
);
Enter fullscreen mode Exit fullscreen mode

40. Authentication Example

Suppose a user logs in.

The UI dispatches:

dispatch(
  loginSuccess({
    id: 1,
    name: "John"
  })
);
Enter fullscreen mode Exit fullscreen mode

The reducer updates:

{
  user: {
    id: 1,
    name: "John"
  },
  isAuthenticated: true
}
Enter fullscreen mode Exit fullscreen mode

Other components can react to this state.

For example:

const user = useSelector(selectCurrentUser);
Enter fullscreen mode Exit fullscreen mode

41. Redux DevTools

One of Redux's biggest advantages is debugging.

Redux DevTools can show:

Action
   ↓
Previous State
   ↓
Action Payload
   ↓
Next State
Enter fullscreen mode Exit fullscreen mode

For example:

cart/addItem
Enter fullscreen mode Exit fullscreen mode

You can inspect:

{
  productId: 10,
  quantity: 2
}
Enter fullscreen mode Exit fullscreen mode

and then compare the previous and next state.

This makes complex state transitions easier to debug.


42. Time-Travel Debugging

Because Redux state transitions are explicit:

State 0
 ↓
Action A
 ↓
State 1
 ↓
Action B
 ↓
State 2
 ↓
Action C
 ↓
State 3
Enter fullscreen mode Exit fullscreen mode

development tools can replay state transitions.

This is one of the conceptual reasons Redux became popular.


43. Common Redux Mistakes

Mistake 1: Putting Everything in Redux

Don't put every UI detail into Redux.

Avoid:

{
  modalIsOpen: true,
  inputValue: "hello",
  hoverState: true
}
Enter fullscreen mode Exit fullscreen mode

unless there is a genuine reason for global access.


Mistake 2: Mutating State Outside Immer

Do not do:

const user = store.getState().user;

user.name = "New Name";
Enter fullscreen mode Exit fullscreen mode

State should be changed through Redux actions and reducers.


Mistake 3: Putting API Calls in Reducers

Avoid:

function reducer(state, action) {
  fetch("/api/users");

  return state;
}
Enter fullscreen mode Exit fullscreen mode

Reducers should remain pure.


Mistake 4: Duplicating Derived Data

Avoid storing:

{
  items: [...],
  itemCount: 5
}
Enter fullscreen mode Exit fullscreen mode

if itemCount can simply be calculated from items.


Mistake 5: Huge Global Slice

Avoid creating:

appSlice.js
Enter fullscreen mode Exit fullscreen mode

with hundreds of unrelated responsibilities.

Prefer:

authSlice
cartSlice
productsSlice
ordersSlice
notificationsSlice
Enter fullscreen mode Exit fullscreen mode

44. Redux vs Context API

React Context and Redux solve related but different problems.

Context

Good for:

Theme
Locale
Authentication context
Simple global configuration
Enter fullscreen mode Exit fullscreen mode

Redux

Useful when you have:

Complex state
Many state transitions
Multiple consumers
Complex async workflows
Need for powerful debugging
Normalized entities
Large application state
Enter fullscreen mode Exit fullscreen mode

Context is not automatically a replacement for Redux.


45. Redux vs useState

useState is excellent for local state.

const [count, setCount] = useState(0);
Enter fullscreen mode Exit fullscreen mode

Redux becomes useful when state needs to be shared and the update logic becomes complex.

Think:

Simple local state
      ↓
useState
Enter fullscreen mode Exit fullscreen mode

versus:

Complex shared application state
      ↓
Redux
Enter fullscreen mode Exit fullscreen mode

46. Redux vs Zustand

Redux and Zustand are both state management solutions.

Redux provides a more structured architecture:

Actions
 ↓
Reducers
 ↓
Store
Enter fullscreen mode Exit fullscreen mode

Zustand generally provides a simpler API with less ceremony.

Redux is often preferred when:

  • The application is large
  • A standardized architecture matters
  • Teams need predictable conventions
  • Advanced Redux tooling is valuable
  • Existing ecosystem integrations are important

Zustand can be attractive when simplicity and minimal boilerplate are priorities.


47. A Practical Redux Mental Model

If you remember only one model, remember this:

STATE
  ↑
REDUCER
  ↑
ACTION
  ↑
DISPATCH
  ↑
UI
Enter fullscreen mode Exit fullscreen mode

Or:

User Interaction
      ↓
    Action
      ↓
    Reducer
      ↓
  New State
      ↓
     UI
Enter fullscreen mode Exit fullscreen mode

48. Complete Mini Project

Let's combine the concepts.

Store

import { configureStore } from "@reduxjs/toolkit";
import cartReducer from "./cartSlice";

export const store = configureStore({
  reducer: {
    cart: cartReducer
  }
});
Enter fullscreen mode Exit fullscreen mode

Slice

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

const cartSlice = createSlice({
  name: "cart",

  initialState: {
    items: []
  },

  reducers: {
    addItem(state, action) {
      const existingItem = state.items.find(
        item => item.id === action.payload.id
      );

      if (existingItem) {
        existingItem.quantity += 1;
      } else {
        state.items.push({
          ...action.payload,
          quantity: 1
        });
      }
    },

    removeItem(state, action) {
      state.items = state.items.filter(
        item => item.id !== action.payload
      );
    },

    clearCart(state) {
      state.items = [];
    }
  }
});

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

export default cartSlice.reducer;
Enter fullscreen mode Exit fullscreen mode

Selector

export const selectCartItems =
  state => state.cart.items;

export const selectCartCount =
  state =>
    state.cart.items.reduce(
      (total, item) => total + item.quantity,
      0
    );
Enter fullscreen mode Exit fullscreen mode

Component

function Cart() {
  const items = useSelector(selectCartItems);
  const count = useSelector(selectCartCount);

  const dispatch = useDispatch();

  return (
    <div>
      <h1>Cart ({count})</h1>

      {items.map(item => (
        <div key={item.id}>
          {item.name}

          <button
            onClick={() =>
              dispatch(removeItem(item.id))
            }
          >
            Remove
          </button>
        </div>
      ))}

      <button
        onClick={() => dispatch(clearCart())}
      >
        Clear Cart
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

This is already a realistic Redux pattern.


49. The Most Important Redux APIs

Modern Redux development commonly revolves around:

configureStore()
createSlice()
createAsyncThunk()
createEntityAdapter()
createSelector()
createApi()
fetchBaseQuery()
Enter fullscreen mode Exit fullscreen mode

React integration commonly uses:

Provider
useSelector()
useDispatch()
Enter fullscreen mode Exit fullscreen mode

You do not need to memorize everything immediately.

Understand the architecture first.


50. Redux in One Diagram

                         ┌──────────────┐
                         │     UI       │
                         └──────┬───────┘
                                │
                         dispatch(action)
                                │
                                ↓
                         ┌──────────────┐
                         │ Middleware   │
                         └──────┬───────┘
                                │
                                ↓
                         ┌──────────────┐
                         │   Reducer    │
                         └──────┬───────┘
                                │
                         New Immutable State
                                │
                                ↓
                         ┌──────────────┐
                         │    Store     │
                         └──────┬───────┘
                                │
                          subscribe/select
                                │
                                ↓
                         ┌──────────────┐
                         │     UI       │
                         └──────────────┘
Enter fullscreen mode Exit fullscreen mode

Conclusion

Redux is fundamentally about predictable state transitions.

The most important concepts are:

Store
Actions
Reducers
Dispatch
Selectors
Middleware
Immutability
Enter fullscreen mode Exit fullscreen mode

Modern Redux development should generally use Redux Toolkit rather than manually writing the older Redux boilerplate.

The key mental model is:

UI
 ↓
dispatch(Action)
 ↓
Middleware
 ↓
Reducer
 ↓
New State
 ↓
Store
 ↓
Selectors
 ↓
UI
Enter fullscreen mode Exit fullscreen mode

Once you understand this flow, advanced Redux concepts such as asynchronous actions, RTK Query, entity adapters, memoized selectors, and large-scale Redux architecture become much easier to understand.

Redux isn't simply a place to put variables.

It is an architecture for making state changes explicit, predictable, traceable, and easier to manage as an application grows.

Top comments (0)