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 ( <button onClick={() => dispatch(increment())}> Increment ); } 22. Complete React + Redux Example function Counter() { const count = useSelector( state => state.counter.value ); const dispatch = useDispatch(); return (
{count}
<button onClick={() => dispatch(increment())} > + <button onClick={() => 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 actions Perform asynchronous operations Dispatch additional actions Handle side effects Integrate external services Conceptually: dispatch(action) β Middleware β Reducer β Store 26. Why Do We Need Middleware? Reducers should be pure. Therefore, things like: fetch() should not normally happen inside reducers. Incorrect: function reducer(state, action) { fetch(β/api/productsβ); return state; } 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)); } }; Then: dispatch(fetchUsers()); The flow becomes: Component β dispatch(fetchUsers()) β Thunk β API Request β dispatch(usersLoaded()) β Reducer β Store 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(); } ); 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; }); } }); Now Redux automatically provides: pending fulfilled rejected 29. Handling API State A common pattern is: { data: [], loading: false, error: null } The state transitions are: Initial β loading = true β API Request β βββββββββββββββββ β β Success Failure β β β β data error 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β }) }) }); Then: const { data, isLoading, error } = useGetUsersQuery(); 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 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) { β¦ } } you can use: const counterSlice = createSlice({ name: βcounterβ, initialState: { value: 0 }, reducers: { increment(state) { state.value++; } } }); 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β } ] } 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β } } } } Redux Toolkit provides: createEntityAdapter() for this use case. 33. createEntityAdapter Example: const usersAdapter = createEntityAdapter(); const initialState = usersAdapter.getInitialState(); You can then use generated reducers and selectors to manage entities efficiently. This is especially useful for: Users Products Orders Messages Notifications 34. Selectors in Large Applications Instead of exposing state structure everywhere: state.products.entities create selectors: const selectProducts = state => state.products.entities; Then components use: const products = useSelector(selectProducts); 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 } ] } You donβt necessarily need: { products: [β¦], totalPrice: 600 } You can derive it: const selectTotalPrice = state => state.products.reduce( (total, product) => total + product.price, 0 ); 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) ); 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/ 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 Example: { auth: { user: null, token: null }, cart: { items: [] }, products: { ids: [], entities: {} }, orders: { ids: [], entities: {} }, ui: { sidebarOpen: false } } 39. Adding a Product to the Cart User clicks: Add to Cart The component dispatches: dispatch( addToCart({ productId: 10, quantity: 1 }) ); Redux flow: Product Component β addToCart() β Action β Cart Reducer β Cart State Updated β Cart Icon β UI Updated The cart icon can now select: const itemCount = useSelector( selectCartItemCount ); 40. Authentication Example Suppose a user logs in. The UI dispatches: dispatch( loginSuccess({ id: 1, name: βJohnβ }) ); The reducer updates: { user: { id: 1, name: βJohnβ }, isAuthenticated: true } Other components can react to this state. For example: const user = useSelector(selectCurrentUser); 41. Redux DevTools One of Reduxβs biggest advantages is debugging. Redux DevTools can show: Action β Previous State β Action Payload β Next State For example: cart/addItem You can inspect: { productId: 10, quantity: 2 } 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 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 } 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β; 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; } Reducers should remain pure. Mistake 4: Duplicating Derived Data Avoid storing: { items: [β¦], itemCount: 5 } if itemCount can simply be calculated from items. Mistake 5: Huge Global Slice Avoid creating: appSlice.js with hundreds of unrelated responsibilities. Prefer: authSlice cartSlice productsSlice ordersSlice notificationsSlice 44. Redux vs Context API React Context and Redux solve related but different problems. Context Good for: Theme Locale Authentication context Simple global configuration Redux Useful when you have: Complex state Many state transitions Multiple consumers Complex async workflows Need for powerful debugging Normalized entities Large application state Context is not automatically a replacement for Redux. 45. Redux vs useState useState is excellent for local state. const [count, setCount] = useState(0); Redux becomes useful when state needs to be shared and the update logic becomes complex. Think: Simple local state β useState versus: Complex shared application state β Redux 46. Redux vs Zustand Redux and Zustand are both state management solutions. Redux provides a more structured architecture: Actions β Reducers β Store 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 Or: User Interaction β Action β Reducer β New State β UI 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 } }); 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; Selector export const selectCartItems = state => state.cart.items; export const selectCartCount = state => state.cart.items.reduce( (total, item) => total + item.quantity, 0 ); Component function Cart() { const items = useSelector(selectCartItems); const count = useSelector(selectCartCount); const dispatch = useDispatch(); return (
Cart ({count})
{items.map(item => (
{item.name} <button onClick={() => dispatch(removeItem(item.id)) } > Remove
))} <button onClick={() => dispatch(clearCart())} > Clear Cart
); } 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() React integration commonly uses: Provider useSelector() useDispatch() 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 β ββββββββββββββββ Conclusion Redux is fundamentally about predictable state transitions. The most important concepts are: Store Actions Reducers Dispatch Selectors Middleware Immutability 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 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.
View original article