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 Instead of allowing components to modify application state however they want, Redux creates a controlled flow. For example: dispatch({ type: "counter/increment" }); The action reaches a reducer: function counterReducer(state, action) { if (action.type === "counter/increment") { return { ...state, value: state.value + 1 }; } return state; } 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 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 This is commonly called prop drilling. Redux provides a centralized store: Redux Store / | \ ↓ ↓ ↓ Header Dashboard Sidebar 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" } } 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++; Instead, dispatch an action: dispatch({ type: "counter/increment" }); 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 Example: const previousState = { value: 10 }; const action = { type: "increment" }; const nextState = { value: 11 }; The reducer: function reducer(state, action) { switch (action.type) { case "increment": return { ...state, value: state.value + 1 }; default: return state; } } 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 } }); Conceptually: Store │ ├── counter ├── user ├── products ├── cart └── notifications 7. Actions An action describes what happened. Example: { type: "counter/increment" } Another example: { type: "cart/addItem", payload: { id: 10, name: "Keyboard" } } The action does not directly modify state. It describes an event. 8. Action Types An action type is usually a string. { type: "user/login" } Examples: user/login user/logout cart/addItem cart/removeItem products/load products/delete A useful naming convention is: feature/event For example: cart/addItem 9. Payload The payload contains additional information. { type: "cart/addItem", payload: { id: 1, name: "Laptop", price: 1200 } } Another example: { type: "user/setUser", payload: { id: 5, name: "John" } } 10. Reducers A reducer receives: Current State + Action and returns: Next State 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; } } A reducer should be: - Predictable - Pure - Deterministic - Free of side effects 11. What Does "Pure Function" Mean? A pure function: - Produces the same output for the same input. - Does not modify external state. - Does not perform side effects. Example: function add(a, b) { return a + b; } This is pure. But: let total = 0; function add(value) { total += value; } 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" }); The flow becomes: Component ↓ dispatch(action) ↓ Redux ↓ Reducer ↓ New State ↓ Store ↓ Subscribed Components 13. Selectors Selectors read data from the Redux store. For example: const selectCount = state => state.counter.value; Then: const count = useSelector(selectCount); Selectors help keep components independent from the exact shape of the state. Instead of: state.counter.value everywhere, you can use: selectCount(state) 14. Redux Data Flow Redux follows a predictable one-way data flow. ┌─────────────┐ │ UI │ └──────┬──────┘ │ │ dispatch() ↓ ┌─────────────┐ │ Action │ └──────┬──────┘ ↓ ┌─────────────┐ │ Reducer │ └──────┬──────┘ ↓ ┌─────────────┐ │ Store │ └──────┬──────┘ ↓ ┌─────────────┐ │ UI │ └─────────────┘ 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; } } }); Export the actions: export const { increment, decrement, incrementByAmount } = counterSlice.actions; Create the store: const store = configureStore({ reducer: { counter: counterSlice.reducer } }); Now: store.dispatch(increment()); Or: store.dispatch(incrementByAmount(10)); 16. Why Does Redux Toolkit Allow Mutation? You might notice: state.value += 1; 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; while internally producing an immutable state update. Conceptually: Your Code ↓ Immer ↓ Immutable Update ↓ Redux State 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) { ... } you can write: const counterSlice = createSlice({ name: "counter", initialState: { value: 0 }, reducers: { increment(state) { state.value++; } } }); 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); } } }); Dispatch: dispatch( addItem({ id: 1, name: "Laptop", price: 1200 }) ); The action becomes conceptually: { type: "cart/addItem", payload: { id: 1, name: "Laptop", price: 1200 } } 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 } }); Then provide it to React: import { Provider } from "react-redux"; 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 {count} ; } 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 ( dispatch(increment())}> Increment ); } 22. Complete React + Redux Example function Counter() { const count = useSelector( state => state.counter.value ); const dispatch = useDispatch(); return ( {count} dispatch(increment())} > + dispatch(decrement())} > - ); } The component does not directly change: state.counter.value Instead: Button ↓ dispatch(increment()) ↓ Reducer ↓ New State ↓ useSelector ↓ Component Re-render 23. Local State vs Redux State Not every piece of state belongs in Redux. For example: const [isOpen, setIsOpen] = useState(false); 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 Global State Examples: Authenticated user Shopping cart Permissions Global notifications Shared application configuration Cached server data 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 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 It can: - Log acti
Comments
No comments yet. Start the discussion.