Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
Redux & State Management
As your React application grows beyond a few components, you’ll encounter challenges with state management. Props drilling becomes unwieldy, Context API re-renders too much, and tracking state changes becomes a nightmare. Redux addresses these problems with a structured approach to global state. Think of Redux like a bank. Instead of every person (component) keeping cash under their mattress (local state) and passing envelopes of money to each other (props), everyone uses a single bank vault (the store). To deposit or withdraw, you fill out a form (dispatch an action). A teller (the reducer) processes your form and updates the ledger. Anyone can check their balance at any time (subscribe to state). The key insight: there is exactly one ledger, and every change goes through the same teller window, so you always have a clear audit trail.When Do You Need Redux?
Redux adds complexity, so it’s important to know when it’s worth it:You Might Need Redux When:
- Multiple components need access to the same state
- State updates come from many different sources (user input, API calls, WebSockets)
- State changes are complex with many interconnected pieces
- Debugging is difficult because you can’t track what changed and when
- Team collaboration requires predictable patterns everyone follows
You Probably Don’t Need Redux When:
- Your app is simple with few components
- State is localized to individual components
- Context API handles your needs without performance issues
- You’re building a prototype or MVP
Redux Core Concepts
Before diving into code, understand the philosophy:| Concept | Description | Analogy |
|---|---|---|
| Store | A single JavaScript object that holds all app state | The bank vault |
| Action | A plain object describing what happened ({ type: 'ADD_ITEM' }) | A filled-out form submitted to the teller |
| Dispatch | The function you call to send an action to the store | Handing the form to the teller |
| Reducer | A pure function that takes current state + action and returns new state | The teller processing your form |
| Selector | A function that reads a specific slice of state | Checking your account balance |
- Single Source of Truth — All app state lives in one store object.
- State is Read-Only — The only way to change state is by dispatching actions. You never mutate the store directly.
- Pure Reducers — Changes are made with pure functions (given same input, always same output, no side effects).
Redux Toolkit (RTK)
We’ll use Redux Toolkit (RTK), the official, recommended way to write Redux logic. It eliminates boilerplate and includes best practices by default.Installation
1. Create a Slice
A “slice” contains the reducer logic and actions for a single feature. Think of it like a department in the bank — the counter department handles counting, the users department handles user accounts, etc. Each slice owns its own piece of state and the rules for changing it. features/counter/counterSlice.js2. Configure the Store
Create the Redux store and add your slices. The store is the single source of truth for your entire application’s state. Think ofconfigureStore as assembling a filing cabinet — each drawer (slice) holds documents for one department, and the cabinet itself is the store.
app/store.js
3. Provide the Store
Wrap your application with theProvider.
main.jsx
4. Use State and Dispatch
UseuseSelector to read data from the store and useDispatch to send actions. These two hooks are the bridge between your React components and the Redux world.
Counter.jsx
Async Logic (Thunks)
Redux reducers must be pure and synchronous — they cannot make API calls. So how do you fetch data? Redux Toolkit includescreateAsyncThunk, which handles the async work and automatically dispatches pending, fulfilled, and rejected actions for you.
Think of it like ordering food: you place the order (dispatch the thunk), the kitchen works on it (async API call), and eventually you either get your meal (fulfilled) or an apology (rejected). The reducer just needs to react to each of those three outcomes.
Why three states? Tracking
pending, fulfilled, and rejected separately lets you show loading spinners, success messages, and error alerts in your UI — all driven by a single dispatched thunk. This is much cleaner than managing isLoading booleans manually.Common Redux Pitfalls
Other Options
While Redux is popular, other libraries exist:- Zustand: Minimalist, hook-based state management.
- Recoil / Jotai: Atomic state management.
- TanStack Query (React Query): Best for server state (caching API responses).
Redux vs. Context API vs. Other Options
Choosing the right state management tool matters. Here is a practical comparison:| Criteria | Context API | Redux Toolkit | Zustand | React Query |
|---|---|---|---|---|
| Best for | Theme, auth, locale | Complex client state | Simple global state | Server/API state |
| Boilerplate | Low | Medium (RTK reduces it) | Very low | Low |
| DevTools | None built-in | Excellent time-travel debugger | Basic | Excellent |
| Re-render control | Re-renders all consumers | Granular with selectors | Granular | Automatic |
| Async support | Manual | createAsyncThunk | Manual | Built-in |
| Learning curve | Low | Medium | Low | Low |
Summary
| Concept | Description |
|---|---|
| Store | Single object holding all application state |
| Slice | A feature-sized bundle of reducer + actions (RTK) |
| Action | A plain object describing what happened |
| Reducer | A pure function that computes the next state |
| Provider | Makes the store available to the component tree |
| useSelector | Reads (subscribes to) a piece of state |
| useDispatch | Sends actions to trigger state updates |
| createAsyncThunk | Handles async operations with auto-generated pending/fulfilled/rejected actions |
Next Steps
In the next chapter, you’ll learn about Custom Hooks & Advanced Patterns — extracting reusable logic into hooks you can share across your entire app!
Interview Deep-Dive
Why must Redux reducers be pure functions? What would break if a reducer had side effects?
Why must Redux reducers be pure functions? What would break if a reducer had side effects?
Strong Answer:
Redux’s value proposition rests on predictability: given the same state and action, a reducer must return the same new state. This enables time-travel debugging (replaying actions produces identical states), hot module reloading, and simple testing.If a reducer made an API call, replaying the action in DevTools would trigger another call — possibly charging a customer twice. If it used
Math.random() or Date.now(), replay produces different state. If it mutated existing state instead of returning a new object, the === check for state changes fails and components do not re-render.Redux Toolkit’s configureStore includes middleware that checks for accidental mutations and non-serializable values in development, throwing errors early.Follow-up: Where DO side effects go in Redux?Thunks are async functions that receive dispatch and getState. createAsyncThunk generates pending/fulfilled/rejected actions automatically. Simple and sufficient for most apps. Sagas use generator functions for complex orchestration like racing actions or debouncing. RTK Query auto-generates hooks with caching, deduplication, and optimistic updates for server state. My recommendation: RTK Query for server data, thunks for occasional client async, sagas only for genuinely complex orchestration.How does useSelector work under the hood? What is the performance difference between selecting entire state versus a specific slice?
How does useSelector work under the hood? What is the performance difference between selecting entire state versus a specific slice?
Strong Answer:
useSelector subscribes to the store via store.subscribe(). On every dispatch, the subscription callback runs the selector function and compares the result to the previous result using ===. If changed, the component re-renders.Selecting entire state — useSelector(state => state) — returns the root object, which is a new reference on every dispatch. The component re-renders on every store update, defeating the purpose of selectors.Selecting a specific slice — useSelector(state => state.counter.value) — returns a primitive. Unrelated slice changes do not affect this value, so the component skips rendering.The subtle case: useSelector(state => state.items.filter(i => i.active)) creates a new array every call. Use createSelector from Reselect to memoize derived data.Follow-up: How does createSelector differ from useMemo?createSelector is a pure function called outside React’s render cycle — usable in tests, shared across components, composable. useMemo is a hook tied to a component instance. createSelector has a cache size of 1 by default, so parameterized selectors may need a factory pattern or larger cache.You join a team that uses Redux for everything -- API caching, form state, UI toggles. What would you change?
You join a team that uses Redux for everything -- API caching, form state, UI toggles. What would you change?
Strong Answer:
I would categorize the state and migrate each to the right tool. Server state moves to React Query or RTK Query for caching, deduplication, and background refetching. Form state moves to component-local state or React Hook Form — dispatching an action on every keystroke adds latency for zero benefit. UI state (modal open/closed) moves to local useState.What stays in Redux: truly global client state read by many unrelated components with complex update logic benefiting from DevTools. Authentication, user preferences, shopping cart with pricing rules.The migration approach: start with highest-churn Redux state (forms, API data) first. Each migration reduces dispatches per second, improving performance.Follow-up: How do you manage the transition period with some state in Redux and some in React Query?Run them side by side with both Providers. Migrate one endpoint at a time: replace the thunk, slice, and selector with a useQuery hook. Components use both hooks where needed:
useQuery for server data and useSelector for client state. No bridging required. The only complication is Redux middleware orchestrating flows that mix server and client data — restructure those as React hooks composing useQuery with useSelector.