Suggest an editImprove this articleRefine the answer for “What is Redux?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Redux** is a predictable state container for JavaScript apps. It centralizes all state updates through pure reducer functions that respond to dispatched action objects. ```javascript const reducer = (state = [], action) => { switch (action.type) { case 'ADD_TODO': return [...state, action.payload]; default: return state; } }; const store = createStore(reducer); store.dispatch({ type: 'ADD_TODO', payload: 'Buy milk' }); // store.getState() → ['Buy milk'] ``` **Key concept:** one global store, updates only through dispatched actions, state never mutated directly.Shown above the full answer for quick recall.Answer (EN)Image**Redux** is a predictable state container for JavaScript apps that centralizes all state changes through pure reducer functions responding to plain action objects. ## Theory ### TL;DR - Redux is like a single company ledger: every change (action) gets recorded by accountants (reducers) who produce a new page (state) without erasing the old one - One global store instead of scattered local state across components - Three moving parts: store (holds state), actions (describe events), reducers (pure update functions) - Use it when state logic repeats across 5+ components or you need time-travel debugging - Redux Toolkit is the modern way to write Redux, cutting roughly 70% of the boilerplate ### Quick example ```javascript import { createStore } from 'redux'; const reducer = (state = [], action) => { switch (action.type) { case 'ADD_TODO': return [...state, action.payload]; // Immutable update default: return state; } }; const store = createStore(reducer); store.dispatch({ type: 'ADD_TODO', payload: 'Buy milk' }); console.log(store.getState()); // ['Buy milk'] store.dispatch({ type: 'ADD_TODO', payload: 'Walk dog' }); console.log(store.getState()); // ['Buy milk', 'Walk dog'] ``` The action describes *what happened*. The reducer decides *what that means for the state*. The store holds the result. ### Key difference Local component state is scattered across the tree. Any component that needs shared data must receive it through props or a context, which gets messy fast. Redux puts everything in one object tree, and the only way to change anything in it is to dispatch an action. That single constraint is what makes large apps debuggable. ### When to use Redux - **Small app, fewer than 5 components sharing state** - skip it. React Context or useState is enough. - **Medium app with repeated state logic** - Redux Toolkit with slices. - **Large app that needs devtools or time-travel debugging** - full Redux with middleware. - **Server-side or non-React JS app** - Redux core works without React bindings. - **State under 10KB and simple** - consider Zustand, it is lighter. ### How Redux works internally When you call `store.dispatch(action)`, Redux passes the action through any registered middleware (thunks, loggers), then hands it to the root reducer. The root reducer calls `combineReducers`, which passes the action to each child reducer. Each child returns a new slice of state. Redux shallow-merges the slices into a new state object and notifies all subscribers. React's `useSelector` compares the new state with the previous one using reference equality, and re-renders only the components whose selected slice actually changed. Pure functions make this fast. Immutable updates allow React.memo and `reselect` to skip unnecessary renders with a simple reference check. ### Common mistakes **Mutating state in a reducer** ```javascript // Wrong - mutates the shared array function badReducer(state = [], action) { if (action.type === 'ADD') state.push(action.payload); // Direct mutation! return state; } // Correct - returns a new array function goodReducer(state = [], action) { if (action.type === 'ADD') return [...state, action.payload]; return state; } ``` Redux shares state references between renders. If you mutate, all subscribers see the change immediately, bypassing the reducer cycle and breaking the whole model. **Dispatching inside render** ```javascript // Wrong - runs on every render, causes an infinite loop function BadComponent() { dispatch(fetchData()); return <div>...</div>; } // Correct - runs once on mount function GoodComponent() { useEffect(() => { dispatch(fetchData()); }, []); return <div>...</div>; } ``` This is one of the most common Redux bugs in production. I have seen it cause hundreds of duplicate API calls in a single session before anyone noticed. **Ignoring Redux Toolkit** Writing vanilla Redux with `createStore` and manual `combineReducers` in 2024 adds a lot of boilerplate for no real gain. Redux Toolkit gives you Immer (mutative syntax that stays immutable), auto-generated action creators, and `createAsyncThunk` for async flows. The Redux team recommends Toolkit for all new projects. **Race conditions in thunks** ```javascript store.dispatch(fetchUser(1)); // dispatched first, responds later store.dispatch(fetchUser(2)); // dispatched second, responds first // Without protection, the slower request overwrites the faster one ``` Fix: store the `requestId` when the request starts, then check in `fulfilled` whether it matches. If not, skip the update. **Over-normalizing small state** Nesting `{ entities: { users: { 1: { name: 'Bob' } } } }` for a single user adds devtools clutter and boilerplate with no benefit. Keep state flat until you have more than 10 related items. ### Real-world usage - **React / Next.js** - global state for auth, cart, notifications (Shopify Hydrogen uses RTK Query) - **Electron apps** - offline-first state sync (Discord desktop is a well-known example) - **Node.js with WebSockets** - shared session state across connections - **Vanilla JS** - game state for deterministic replays (common in Phaser-based games) - **Alternatives** - Zustand for apps under 500 lines, Jotai for atomic state, TanStack Query when most state is server data ### Follow-up questions **Q:** Walk me through the Redux flow from `dispatch` to a React re-render. **A:** Action hits dispatch, passes through middleware (e.g. thunk resolves async work), reaches the root reducer, which calls child reducers via `combineReducers`, each returning a new slice. Redux builds a new state object, notifies subscribers. React's `useSelector` runs the selector, compares by reference, and re-renders the component if the result changed. **Q:** Why do reducers have to be pure functions? **A:** Pure functions return the same output for the same input and have no side effects. That property makes time-travel debugging possible: replay any sequence of actions and you get the same state. It also lets Redux use shallow equality checks for performance and makes unit testing straightforward. **Q:** What does Redux Toolkit add over vanilla Redux? **A:** Immer integration (write mutative syntax, get immutable updates), `createSlice` (auto-generates action creators and types), `configureStore` (adds Redux DevTools and thunk middleware by default), `createAsyncThunk` (handles pending/fulfilled/rejected states), and RTK Query (data fetching with caching). It cuts roughly 70% of boilerplate. **Q:** How would you implement `combineReducers` yourself? **A:** ```javascript function combineReducers(reducers) { return (state = {}, action) => { return Object.keys(reducers).reduce((nextState, key) => { nextState[key] = reducers[key](state[key], action); return nextState; }, {}); }; } ``` Each key gets its own slice of state. The root reducer calls each child and assembles the result. **Q:** How do you handle race conditions in async thunks? **A:** Redux Toolkit's `createAsyncThunk` attaches a `requestId` to each dispatched thunk. Store the latest `requestId` in state when the request starts. In the `fulfilled` case, check: `if (action.meta.requestId !== state.currentRequestId) return;`. This drops responses from stale requests. ## Examples ### Basic Redux flow ```javascript import { createStore } from 'redux'; const reducer = (state = { count: 0 }, action) => { switch (action.type) { case 'INCREMENT': return { count: state.count + 1 }; case 'DECREMENT': return { count: state.count - 1 }; default: return state; } }; const store = createStore(reducer); store.subscribe(() => console.log(store.getState())); store.dispatch({ type: 'INCREMENT' }); // { count: 1 } store.dispatch({ type: 'INCREMENT' }); // { count: 2 } store.dispatch({ type: 'DECREMENT' }); // { count: 1 } ``` The store holds one object. Every dispatch goes through the reducer. State never changes in place. ### Todo list with Redux Toolkit ```javascript import { configureStore, createSlice } from '@reduxjs/toolkit'; import { useSelector, useDispatch } from 'react-redux'; const todosSlice = createSlice({ name: 'todos', initialState: [], reducers: { addTodo: (state, action) => { state.push(action.payload); }, // Immer handles immutability toggleTodo: (state, action) => { state[action.payload].completed = true; } } }); const store = configureStore({ reducer: { todos: todosSlice.reducer } }); function TodoList() { const todos = useSelector(state => state.todos); const dispatch = useDispatch(); return ( <ul> {todos.map((todo, i) => ( <li key={i} onClick={() => dispatch(todosSlice.actions.toggleTodo(i))}> {todo.text} </li> ))} <button onClick={() => dispatch(todosSlice.actions.addTodo({ text: 'New task', completed: false }))}> Add </button> </ul> ); } ``` `createSlice` generates action creators automatically. Immer intercepts the `state.push(...)` call inside the reducer and applies it as an immutable update behind the scenes. ### Async thunk with race condition handling ```javascript import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; const fetchUser = createAsyncThunk('user/fetch', async (id) => { const response = await fetch(`/api/user/${id}`); return response.json(); }); const userSlice = createSlice({ name: 'user', initialState: { data: null, loading: false, currentRequestId: null }, extraReducers: (builder) => { builder .addCase(fetchUser.pending, (state, action) => { state.loading = true; state.currentRequestId = action.meta.requestId; // Track the latest request }) .addCase(fetchUser.fulfilled, (state, action) => { if (action.meta.requestId !== state.currentRequestId) return; // Drop stale responses state.data = action.payload; state.loading = false; }); } }); ``` Without the `requestId` check, a slow first request can override the result of a faster second one. This pattern prevents that overwrite.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.