Suggest an editImprove this articleRefine the answer for “Redux vs context API”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Redux vs Context API**: Context API shares state through React's provider tree with no extra dependencies; Redux manages state through actions and reducers in a separate store. ```jsx // Context: all consumers re-render on any provider value change const theme = useContext(AppContext); // Redux: only subscribed components re-render const theme = useSelector(state => state.theme); ``` **Key rule:** simple shared state (theme, auth) with few components? Context. Async logic, frequent updates, or a team-size app? Redux with RTK.Shown above the full answer for quick recall.Answer (EN)Image**Redux vs Context API**: Redux is a centralized state container that manages data through actions and reducers in a separate store; Context API is React's built-in way to share data down the component tree without prop drilling. ## Theory ### TL;DR - Context API is like a family group chat: simple, everyone sees everything, easy to set up. Redux is like a company Slack with channels and logs: structured, built for teams. - Main difference: Context re-renders every consumer when the provider value changes; Redux re-renders only components subscribed to the specific slice of state via `useSelector`. - Under 5-10 components sharing simple state? Use Context. Complex async logic, 10+ components, or team debugging needs? Use Redux. - Redux adds boilerplate but gives you time-travel debugging via Redux DevTools; Context gives you nothing built-in. - Modern Redux Toolkit (RTK) cuts the ceremony significantly compared to legacy Redux. ### Quick Example The core difference is in how re-renders are triggered: ```jsx // Context API - ALL consumers re-render on ANY value change const AppContext = React.createContext(); function App() { const [theme, setTheme] = useState('light'); const [user, setUser] = useState(null); return ( // One shared object - any change triggers all consumers <AppContext.Provider value={{ theme, user }}> <ThemeConsumer /> {/* Re-renders on theme OR user change */} <UserConsumer /> {/* Re-renders on theme OR user change */} </AppContext.Provider> ); } // Redux - only the subscribed component re-renders function ThemeConsumer() { const theme = useSelector(state => state.theme); // Only re-renders on theme change return <div>{theme}</div>; } ``` When `user` changes in the Context example, `ThemeConsumer` re-renders too, even though it has no use for `user`. Redux's `useSelector` skips that re-render entirely. ### Key Difference Redux enforces unidirectional data flow: you dispatch an action, a pure reducer computes the next state, and only components subscribed to that slice re-render. Context just shares a value down the tree. It has no concept of actions, reducers, or selective subscriptions. Any change to the provider's value object triggers a re-render in every consumer below it. That is by design, not a bug, but it becomes a real problem as the app grows. ### When to Use - Simple theme toggle or auth status across 1-5 components: Context API. - Form state shared between sibling components with no async logic: Context API. - App-wide state with API calls, loading flags, and error handling: Redux with `createAsyncThunk`. - 10+ components sharing state that updates frequently: Redux + Reselect. - Team project with debugging and code review needs: Redux (the action log alone is worth it). - Performance-sensitive lists with frequent updates: Redux + Reselect. ### Comparison Table | Feature | Context API | Redux (RTK) | |---|---|---| | **Setup** | `createContext()` + Provider, zero extra deps | `@reduxjs/toolkit` + `configureStore()` | | **Boilerplate** | Minimal | Medium (slices, actions) | | **Re-renders** | All consumers on value change | Only matching `useSelector` calls | | **DevTools** | None built-in | Time-travel, action log (Redux DevTools) | | **Async handling** | Manual (`useEffect` + local state) | `createAsyncThunk` built-in | | **Scalability** | Small to medium apps | Large apps, teams | | **When to use** | Themes, auth token, locale | E-commerce cart, dashboard with API sync | ### How It Works Internally React Context uses the fiber tree: the Provider writes to `context._currentValue`, and every consumer walks up the fiber stack to find the nearest Provider. Any change to the provider's value object fails a shallow equality check and triggers `scheduleUpdateOnFiber` for all consumers below. Redux stores state in a plain JS object (wrapped with Immer in RTK for safe mutations in place). When you dispatch an action, the store runs the reducers, then notifies subscribers. `useSelector` compares previous and next selected values with strict equality - if equal, the component skips re-render. In React 18, Redux batches updates using `unstable_batchedUpdates`, so multiple dispatches in the same event handler do not cause multiple render cycles. ### Common Mistakes **Putting all state in one Context object:** ```jsx // Wrong - new object reference on every render <Context.Provider value={{ theme, user, cart }}> // Fix - memoize the value const value = useMemo(() => ({ theme, user }), [theme, user]); <Context.Provider value={value}> ``` A new object literal is created on every render, so shallow equality always fails and all consumers re-render, even when nothing relevant changed. **Using Context for high-frequency updates:** ```jsx // Wrong - 60fps typing re-renders the entire consumer subtree const [searchQuery, setSearchQuery] = useState(''); <SearchContext.Provider value={{ searchQuery, setSearchQuery }}> ``` Keep high-frequency state local. Put only the settled final value in Context if other components need it. **Selecting too broadly in Redux:** ```jsx // Wrong - re-renders on any store change at all const todos = useSelector(state => state.todos); // Fix - select the exact slice you need const todoList = useSelector(state => state.todos.list); ``` **Mutating state directly in a plain Redux reducer:** ```jsx // Wrong - breaks Redux DevTools and state predictability reducer: (state, action) => { state.arr.push(action.payload); // Direct mutation return state; } // Fix with RTK - Immer handles this safely reducers: { addItem: (state, action) => { state.arr.push(action.payload); // RTK/Immer converts this to an immutable update } } ``` In plain Redux without RTK, always return a new object. With RTK, the Immer draft lets you write mutation-style code that produces an immutable result. ### Real-World Usage - React docs: `ThemeContext` for simple theming across a component tree. - Next.js apps: auth context for sharing session and user object. - Shopify Polaris: Redux for centralized merchant state across admin panels. - VS Code extensions: Redux for workspace state shared across editor panels. - RTK Query in larger apps: API caching for posts and comments with automatic cache invalidation. I saw this pattern play out on a mid-size product: we started with Context for auth, then added it for cart, then for notifications. By the time we had 4 nested contexts, the component tree was unreadable and debugging was guesswork. We migrated to Redux, and the action log alone justified the effort. ### Follow-Up Questions **Q:** How does Context cause unnecessary re-renders, and what are the fixes? **A:** Any change to the provider's value object triggers re-renders in all consumers, because object literals fail shallow equality checks on every render. Fixes: memoize the value with `useMemo`, split into multiple Contexts by update frequency, or switch to a library like Zustand for that piece of state. **Q:** Walk through the Redux flow: action, thunk, reducer, component. **A:** You dispatch an action (a plain object with a `type` field). If async, a thunk middleware intercepts it, does the async work, then dispatches a regular action. The reducer receives it and returns new state. Components with `useSelector` check if their selected slice changed - if yes, they re-render. **Q:** Why pick Redux over Zustand or Jotai for a large app? **A:** Redux DevTools with time-travel debugging, a mature middleware ecosystem (RTK Query, redux-saga), and well-established patterns for teams. Zustand is lighter and works well for medium apps, but lacks the DevTools depth and predictability guarantees that Redux provides at scale. **Q:** Performance: Context vs Redux with 1000 items? **A:** Redux with Reselect normalizes lookups to roughly O(1) via memoized selectors. Context triggers an O(n) fiber tree traversal on every value change. For large lists with frequent updates, the difference becomes visible in profiling. **Q:** In React 18 concurrent mode, how does Redux batching interact with `startTransition`? **A:** Redux uses `unstable_batchedUpdates` to batch dispatch calls. With concurrent mode, `startTransition` marks updates as non-urgent, so the UI stays responsive during heavy selector computation. For async thunks that need to show UI feedback like a loading spinner, pair `useTransition` with the dispatch so the spinner renders immediately while the data loads in the background. ## Examples ### Theme Toggle with Context API ```jsx const ThemeContext = React.createContext(); function ThemeProvider({ children }) { const [theme, setTheme] = useState('light'); // Memoize to prevent re-renders from new object references const value = useMemo(() => ({ theme, setTheme }), [theme]); return ( <ThemeContext.Provider value={value}> {children} </ThemeContext.Provider> ); } function ThemeButton() { const { theme, setTheme } = useContext(ThemeContext); return ( <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}> Current theme: {theme} </button> ); } ``` Theme changes rarely, consumer count is small, and there is no async logic. This is the right fit for Context. ### Todos with API (Redux Toolkit) ```jsx import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; export const fetchTodos = createAsyncThunk('todos/fetch', async () => { const res = await fetch('/api/todos'); return res.json(); }); const todosSlice = createSlice({ name: 'todos', initialState: { list: [], filter: 'all', loading: false }, reducers: { setFilter: (state, action) => { state.filter = action.payload; // Immer handles immutability } }, extraReducers: (builder) => { builder .addCase(fetchTodos.pending, (state) => { state.loading = true; }) .addCase(fetchTodos.fulfilled, (state, action) => { state.list = action.payload; state.loading = false; }); } }); // Component subscribes only to what it needs function TodoList() { const list = useSelector(state => state.todos.list); // Ignores filter changes const dispatch = useDispatch(); useEffect(() => { dispatch(fetchTodos()); }, [dispatch]); return <ul>{list.map(todo => <li key={todo.id}>{todo.title}</li>)}</ul>; } ``` `TodoList` re-renders only when `todos.list` changes. Changing the filter does not touch it. With Context, both components would share the same value object and both would re-render on every state update. ### Object References and Memoization Edge Case ```jsx // Context - new object on every render breaks React.memo function BadProvider({ children }) { const [count, setCount] = useState(0); return ( // New object every render - React.memo on Child won't prevent re-renders <Context.Provider value={{ count, increment: () => setCount(c => c + 1) }}> <Child /> </Context.Provider> ); } // Redux - Immer produces a new reference only when data actually changed const counterSlice = createSlice({ name: 'counter', initialState: { count: 0 }, reducers: { increment: state => { state.count += 1; } // Only count ref changes } }); // MemoizedChild skips re-render when unrelated slices update const MemoizedChild = React.memo(function Child() { const count = useSelector(state => state.counter.count); return <div>{count}</div>; }); ``` Immer produces a new object reference only when data actually changed. Combined with a narrow selector, `React.memo` behaves the way you'd expect it to.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.