Redux Interview Questions (Free Preview)
Free sample of 15 from 49 questions available
React-Redux Integration
What is the difference between useSelector and connect()?
The 30-Second Answer: useSelector is a modern hook that lets you extract state directly in functional components, while connect() is a legacy HOC that wraps components to inject state and dispatch as props. Hooks offer simpler syntax and better TypeScript support, but connect() provides built-in optimization with memoized mapStateToProps.
The 2-Minute Answer (If They Want More): The fundamental difference is the programming model. connect() follows the Higher-Order Component pattern, wrapping your component and passing state/dispatch as props. useSelector follows the hooks model, allowing you to access state directly inside your component function. This makes hooks more straightforward and eliminates wrapper hell.
From a performance perspective, connect() automatically memoizes the result of mapStateToProps based on props and state, preventing unnecessary recalculations. useSelector runs on every store update and relies on reference equality checks, meaning you need to handle memoization yourself with reselect or useMemo. However, useSelector's simpler model often makes optimization easier to reason about.
TypeScript support is dramatically better with hooks. With connect(), you need complex type definitions and generics to get proper typing. With useSelector, TypeScript naturally infers types from your selector function, and you can create typed hooks for even better DX.
The hooks API also allows more flexibility - you can use conditional hooks (with proper rules), have multiple selectors without merging logic, and access state alongside other hooks naturally. connect() requires all state selection to happen in mapStateToProps, which can become unwieldy.
That said, connect() isn't deprecated - it's still maintained and works perfectly fine. The Redux team recommends hooks for new code, but there's no urgent need to migrate existing connect() code.
Code Example:
// Old way: connect() HOC
import { connect } from 'react-redux';
import { increment, decrement } from './actions';
class CounterClass extends React.Component {
render() {
const { count, increment, decrement } = this.props;
return (
<div>
<p>{count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
}
const mapStateToProps = (state) => ({
count: state.counter.value
});
const mapDispatchToProps = {
increment,
decrement
};
export default connect(mapStateToProps, mapDispatchToProps)(CounterClass);
// New way: useSelector and useDispatch hooks
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from './actions';
function CounterHooks() {
const count = useSelector(state => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<p>{count}</p>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(decrement())}>-</button>
</div>
);
}
export default CounterHooks;
// Complex example showing differences
// connect() version
const mapStateToPropsMerge = (state, ownProps) => {
const todos = state.todos.filter(t => t.categoryId === ownProps.categoryId);
return {
todos,
totalCount: todos.length,
completedCount: todos.filter(t => t.completed).length
};
};
const TodoListConnect = connect(mapStateToPropsMerge)(
function TodoList({ todos, totalCount, completedCount }) {
return (
<div>
<p>Total: {totalCount}, Completed: {completedCount}</p>
<ul>
{todos.map(todo => <li key={todo.id}>{todo.text}</li>)}
</ul>
</div>
);
}
);
// useSelector version
function TodoListHooks({ categoryId }) {
const todos = useSelector(state =>
state.todos.filter(t => t.categoryId === categoryId)
);
const totalCount = todos.length;
const completedCount = todos.filter(t => t.completed).length;
return (
<div>
<p>Total: {totalCount}, Completed: {completedCount}</p>
<ul>
{todos.map(todo => <li key={todo.id}>{todo.text}</li>)}
</ul>
</div>
);
}
// TypeScript comparison
// connect() - complex types
import { ThunkDispatch } from 'redux-thunk';
import { AnyAction } from 'redux';
interface StateProps {
count: number;
}
interface DispatchProps {
increment: () => void;
}
interface OwnProps {
label: string;
}
type Props = StateProps & DispatchProps & OwnProps;
const CounterTS: React.FC<Props> = ({ count, increment, label }) => (
<div>
<p>{label}: {count}</p>
<button onClick={increment}>+</button>
</div>
);
const mapState = (state: RootState): StateProps => ({
count: state.counter.value
});
const mapDispatch = (
dispatch: ThunkDispatch<RootState, void, AnyAction>
): DispatchProps => ({
increment: () => dispatch(increment())
});
export default connect<StateProps, DispatchProps, OwnProps, RootState>(
mapState,
mapDispatch
)(CounterTS);
// useSelector - simple types with typed hooks
import { useAppSelector, useAppDispatch } from './hooks';
interface Props {
label: string;
}
const CounterHooksTS: React.FC<Props> = ({ label }) => {
const count = useAppSelector(state => state.counter.value); // Fully typed!
const dispatch = useAppDispatch();
return (
<div>
<p>{label}: {count}</p>
<button onClick={() => dispatch(increment())}>+</button>
</div>
);
};
Comparison Table:
| Feature | useSelector/useDispatch | connect() |
|---|---|---|
| Syntax | Simple, direct | HOC wrapper |
| TypeScript | Excellent, natural inference | Complex generics needed |
| Memoization | Manual (reselect/useMemo) | Automatic in mapStateToProps |
| Performance | Selector runs every update | mapStateToProps memoized |
| Multiple selectors | Easy, multiple calls | Merge in mapStateToProps |
| Conditional logic | Can use hooks rules | Must happen in mapStateToProps |
| Component props | Passed directly | Merged with state/dispatch |
| Debugging | Easier to trace | Extra HOC layer |
| Learning curve | Gentler (if know hooks) | Steeper (HOC pattern) |
| Recommendation | Use for new code | Maintain existing code |
Mermaid Diagram:
flowchart TD
subgraph "useSelector/useDispatch (Modern)"
A1[Component Function] -->|useSelector| B1[Direct State Access]
A1 -->|useDispatch| C1[Direct Dispatch Access]
B1 -->|Returns| A1
C1 -->|Returns| A1
end
subgraph "connect HOC (Legacy)"
A2[Component] -->|Wrapped by| B2[connect HOC]
B2 -->|mapStateToProps| C2[State as Props]
B2 -->|mapDispatchToProps| D2[Dispatch as Props]
C2 -->|Passed to| A2
D2 -->|Passed to| A2
end
E[Redux Store] -.->|Subscribes| B1
E -.->|Subscribes| B2
References:
↑ Back to topWhat is the Provider component and how does it work?
The 30-Second Answer: The Provider component uses React Context to make the Redux store accessible to all components in your application tree. You wrap your root component with Provider and pass it your store, enabling any nested component to access the store via hooks or connect.
The 2-Minute Answer (If They Want More): Provider is a React component that leverages React's Context API to pass the Redux store down the component tree without prop drilling. It creates a React Context and provides the store as the context value, which React-Redux hooks and connect() can then consume.
Internally, Provider does more than just simple context provision. It includes subscription management, ensuring that connected components receive updates in the correct order. It also implements optimizations to prevent unnecessary context updates - the context value is only updated when the store instance changes, not on every state update.
Provider should be placed as high as possible in your component tree, typically wrapping your root App component. You can have multiple Provider instances if you need multiple stores (though this is rare and generally discouraged), and each Provider creates its own isolated subscription tree.
The component is simple to use but powerful - it handles all the complexity of managing subscriptions, cleanup, and ensuring components stay in sync with the store.
Code Example:
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';
import App from './App';
// Create your Redux store
const store = configureStore({
reducer: {
user: userReducer,
posts: postsReducer,
comments: commentsReducer
}
});
// Wrap your app with Provider
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
// Multiple Providers (rare case - separate stores)
function MultiStoreApp() {
const mainStore = configureStore({ reducer: mainReducer });
const adminStore = configureStore({ reducer: adminReducer });
return (
<Provider store={mainStore}>
<MainApp />
<Provider store={adminStore}>
<AdminPanel />
</Provider>
</Provider>
);
}
// Accessing the store in child components
import { useSelector, useDispatch } from 'react-redux';
function UserProfile() {
// These hooks work because Provider makes store available
const user = useSelector(state => state.user);
const dispatch = useDispatch();
return <div>{user.name}</div>;
}
Mermaid Diagram:
flowchart TD
A[Redux Store] -->|Passed as prop| B[Provider Component]
B -->|React Context| C[Context.Provider]
C -->|Context value available| D[Component Tree]
D --> E[Child Component A]
D --> F[Child Component B]
D --> G[Deeply Nested Component]
E -->|useSelector/useDispatch| H[Access Store]
F -->|useSelector/useDispatch| H
G -->|useSelector/useDispatch| H
H -.->|Subscribed to| A
References:
↑ Back to topRedux Fundamentals
What is the Redux data flow and how does it work?
The 30-Second Answer: Redux follows a strict unidirectional data flow: (1) User interaction dispatches an action, (2) The store calls the reducer with current state and the action, (3) The reducer returns new state, (4) The store saves the new state and notifies subscribers, (5) UI components re-render with updated state. This cycle repeats for every state change.
The 2-Minute Answer (If They Want More): The Redux data flow is a one-way cycle that ensures predictable state updates. It starts when something happens in your application—a user clicks a button, data arrives from an API, or a timer fires. This event triggers an action dispatch, sending an action object to the store that describes what happened.
When the store receives the dispatched action, it calls the root reducer function with the current state tree and the action. The reducer examines the action type, calculates the appropriate state changes, and returns a completely new state object (never mutating the existing one). If you're using combined reducers, each slice reducer handles its portion of state independently.
After the reducer returns the new state, the store replaces its current state with this new state. The store then notifies all subscribed listeners that state has changed. In React applications, React-Redux has subscribed to the store and triggers re-renders of components that depend on the changed state slices.
Components that used useSelector() to read state will re-run their selectors. If the selected values have changed (determined by shallow equality comparison), those components re-render with the new data. This completes the cycle, updating the UI to reflect the new application state.
This unidirectional flow prevents the chaotic state updates that can happen with bidirectional data binding. Every state change follows the same path (action → reducer → new state → UI), making it easy to trace bugs, implement logging middleware, enable time-travel debugging, and understand exactly how data moves through your application.
Code Example:
import { configureStore, createSlice } from '@reduxjs/toolkit';
import { useSelector, useDispatch } from 'react-redux';
// 1. Define reducer logic
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 const { increment, decrement, incrementByAmount } = counterSlice.actions;
// 2. Create store
const store = configureStore({
reducer: {
counter: counterSlice.reducer
}
});
// 3. React component demonstrates the data flow
function Counter() {
// Step 5: Subscribe to store, get current state
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
const handleIncrement = () => {
// Step 1: User interaction triggers action dispatch
console.log('Step 1: Dispatching action');
dispatch(increment());
// Flow continues automatically...
};
// Step 6: Component renders with updated state
return (
<div>
<h1>Count: {count}</h1>
<button onClick={handleIncrement}>+</button>
<button onClick={() => dispatch(decrement())}>-</button>
<button onClick={() => dispatch(incrementByAmount(5))}>+5</button>
</div>
);
}
// Demonstrating the full flow programmatically
console.log('Initial state:', store.getState());
// { counter: { value: 0 } }
// Step 1: Dispatch action
store.dispatch(increment());
// Behind the scenes:
// Step 2: Store calls reducer with (currentState, action)
// Step 3: Reducer returns new state
// Step 4: Store updates and notifies subscribers
console.log('After increment:', store.getState());
// { counter: { value: 1 } }
// Middleware can intercept the flow
const loggerMiddleware = (storeAPI) => (next) => (action) => {
console.log('Dispatching:', action);
console.log('Current state:', storeAPI.getState());
// Call the next middleware or reducer
const result = next(action);
console.log('New state:', storeAPI.getState());
return result;
};
// Async data flow with thunks
function fetchUserData(userId) {
// Thunk returns a function instead of action object
return async (dispatch, getState) => {
// Step 1: Dispatch loading action
dispatch({ type: 'user/fetchStart' });
try {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
// Step 2: Dispatch success action with data
dispatch({ type: 'user/fetchSuccess', payload: data });
// Flow continues: reducer updates state, UI re-renders
} catch (error) {
dispatch({ type: 'user/fetchError', payload: error.message });
}
};
}
// Visualizing the subscription mechanism
const unsubscribe = store.subscribe(() => {
console.log('State changed! New state:', store.getState());
// In React apps, React-Redux handles this subscription
});
store.dispatch(increment());
// Console: "State changed! New state: { counter: { value: 2 } }"
unsubscribe(); // Stop listening
Mermaid Diagram:
flowchart TD
UI[UI Component<br/>User Interaction] -->|1. Dispatch| Action[Action Object<br/>type: 'increment']
Action -->|2. Send to| Store[Redux Store]
Store -->|3. Call with<br/>current state + action| Reducer[Reducer Function]
Reducer -->|4. Return| NewState[New State<br/>Immutable Update]
NewState -->|5. Update| Store
Store -->|6. Notify| Subscribers[Subscribed Listeners<br/>React-Redux]
Subscribers -->|7. Re-render| UI
UI -.8. Next interaction<br/>starts cycle again.-> Action
Middleware[Middleware<br/>Logging, Thunks] -.Intercept.-> Action
style Store fill:#764abc,color:#fff
style Reducer fill:#62dafb,color:#000
style UI fill:#90EE90
style Action fill:#ffd700,color:#000
References:
- Redux Fundamentals - Data Flow
- Redux Essentials - App Structure
- Redux FAQ - General: When should I use Redux?
What is Redux and what problem does it solve?
The 30-Second Answer: Redux is a predictable state container for JavaScript applications that centralizes application state in a single store. It solves the problem of managing complex state across multiple components by providing a structured, traceable way to update state through actions and reducers, making state changes predictable and debuggable.
The 2-Minute Answer (If They Want More): Redux addresses the challenges that arise when building large-scale applications where state needs to be shared across many components. Without Redux, passing state through multiple component layers (prop drilling) becomes unwieldy, and managing state in multiple locations leads to inconsistencies and bugs that are hard to trace.
Redux enforces a unidirectional data flow where all state lives in a single source of truth (the store), and state can only be modified by dispatching actions that describe what happened. This architectural constraint makes it easier to understand how data flows through your application, implement features like undo/redo, time-travel debugging, and maintain consistent state across your entire app.
While Redux is often associated with React, it's framework-agnostic and can be used with any JavaScript framework or vanilla JS. It's particularly valuable in applications with complex state logic, multiple data sources, or when you need predictable state management that scales well as your application grows.
Modern Redux Toolkit (RTK) has simplified Redux's traditionally verbose API, making it more approachable while maintaining the core benefits of predictable state management.
Code Example:
// Without Redux: Prop drilling nightmare
function App() {
const [user, setUser] = useState(null);
return <Header user={user} setUser={setUser} />;
}
function Header({ user, setUser }) {
return <Navigation user={user} setUser={setUser} />;
}
function Navigation({ user, setUser }) {
return <UserMenu user={user} setUser={setUser} />;
}
// With Redux: Direct access to state from any component
import { useSelector, useDispatch } from 'react-redux';
import { setUser } from './userSlice';
function UserMenu() {
// Access state directly without prop drilling
const user = useSelector(state => state.user);
const dispatch = useDispatch();
const handleLogin = (userData) => {
// Dispatch action to update state
dispatch(setUser(userData));
};
return <div>{user?.name || 'Guest'}</div>;
}
Mermaid Diagram:
flowchart TD
A[Component A needs user state] --> Store[Redux Store<br/>Single Source of Truth]
B[Component B needs user state] --> Store
C[Component C updates user] --> D[Dispatch Action]
D --> Store
Store --> A
Store --> B
Store --> C
style Store fill:#764abc,color:#fff
References:
- Redux Official Documentation - Getting Started
- Redux FAQ: When should I use Redux?
- You Might Not Need Redux - Dan Abramov
Redux vs Alternatives
What is the difference between Redux and Context API?
The 30-Second Answer: Redux is a dedicated state management library with a single store, strict unidirectional data flow, middleware support, and time-travel debugging, while Context API is React's built-in feature for passing data through the component tree without props drilling. Redux excels at complex state with many updates across components, while Context API works well for simpler, less frequently changing data like themes or user preferences.
The 2-Minute Answer (If They Want More):
Redux and Context API solve similar problems but with different philosophies and capabilities. Redux enforces a strict architecture with actions, reducers, and a single immutable store, making state changes predictable and traceable. It includes powerful developer tools, middleware for async operations, and optimized re-render patterns through selector functions.
Context API, being part of React core, has zero external dependencies and a simpler mental model. You create a context, provide values at some level of your component tree, and consume them in child components. However, every context value change triggers re-renders of all consuming components, which can cause performance issues with frequently updating state.
Redux shines when you have complex state interactions, need middleware for side effects (like Redux Thunk or Redux Saga), want time-travel debugging, or require strict patterns for large teams. Context API is perfect for dependency injection, theming, localization, or authentication state that changes infrequently.
The key architectural difference is that Redux separates state management completely from React, while Context API is tightly coupled to React's component lifecycle. This makes Redux more portable and testable in isolation, but Context API more straightforward for simpler use cases.
Code Example:
// Redux Implementation
import { createStore } from 'redux';
import { Provider, useSelector, useDispatch } from 'react-redux';
// Reducer with clear action handling
const counterReducer = (state = { count: 0, lastUpdated: null }, action) => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1, lastUpdated: new Date().toISOString() };
case 'DECREMENT':
return { count: state.count - 1, lastUpdated: new Date().toISOString() };
default:
return state;
}
};
const store = createStore(counterReducer);
// App wrapper with Redux Provider
function App() {
return (
<Provider store={store}>
<Counter />
</Provider>
);
}
// Component using Redux hooks
function Counter() {
const count = useSelector(state => state.count); // Selector for optimized re-renders
const dispatch = useDispatch();
return (
<div>
<p>Count: {count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
</div>
);
}
// Context API Implementation
import { createContext, useContext, useState } from 'react';
// Create context for state sharing
const CounterContext = createContext();
// Custom provider component
function CounterProvider({ children }) {
const [count, setCount] = useState(0);
const [lastUpdated, setLastUpdated] = useState(null);
const increment = () => {
setCount(c => c + 1);
setLastUpdated(new Date().toISOString());
};
const decrement = () => {
setCount(c => c - 1);
setLastUpdated(new Date().toISOString());
};
// All consuming components re-render when any value changes
return (
<CounterContext.Provider value={{ count, lastUpdated, increment, decrement }}>
{children}
</CounterContext.Provider>
);
}
// Custom hook for consuming context
function useCounter() {
const context = useContext(CounterContext);
if (!context) throw new Error('useCounter must be used within CounterProvider');
return context;
}
function Counter() {
const { count, increment, decrement } = useCounter();
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
Comparison Table:
| Feature | Redux | Context API |
|---|---|---|
| Setup Complexity | High - requires store, reducers, actions | Low - built into React |
| Dependencies | External library (~10kb) | None - React native |
| State Updates | Immutable with reducers | Any pattern (usually useState) |
| Re-render Optimization | Excellent with selectors | Poor - all consumers re-render |
| DevTools | Redux DevTools with time-travel | React DevTools only |
| Middleware Support | Yes - thunk, saga, etc. | No - manual implementation |
| Async Handling | Built-in with middleware | Manual with useEffect |
| Boilerplate | High (actions, reducers, types) | Low |
| Best For | Complex, frequently changing state | Simple, infrequent updates |
| Learning Curve | Steep | Gentle |
| Performance at Scale | Excellent with proper selectors | Can degrade without optimization |
| Testing | Easy - pure functions | Harder - requires provider wrapper |
Mermaid Diagram:
flowchart TD
subgraph Redux["Redux Architecture"]
A1[Component] -->|dispatch action| B1[Action]
B1 --> C1[Middleware]
C1 --> D1[Reducer]
D1 -->|updates| E1[Store]
E1 -->|selector| A1
end
subgraph Context["Context API Architecture"]
A2[Provider Component] -->|value prop| B2[Context]
B2 -->|useContext hook| C2[Consumer Component 1]
B2 -->|useContext hook| D2[Consumer Component 2]
A2 -->|state update| A2
end
style E1 fill:#4CAF50
style B2 fill:#2196F3
References:
↑ Back to topWhat is the difference between Redux and Zustand?
The 30-Second Answer: Redux uses a single store with actions and reducers following strict patterns, while Zustand offers a minimalist, hook-based API with less boilerplate and a more flexible approach to state management. Zustand is significantly smaller (~1kb vs ~10kb), doesn't require providers, and supports both immutable and mutable state updates, making it faster to implement for most modern React applications.
The 2-Minute Answer (If They Want More):
Redux and Zustand are both state management solutions, but they represent different philosophies. Redux follows a strict unidirectional data flow with mandatory actions, reducers, and a single source of truth. Every state change must go through a reducer function, making state updates predictable and traceable. This rigidity is valuable for large teams and complex applications where consistency and debugging are paramount.
Zustand takes a minimalist approach, providing a simple hook-based API that feels more natural in modern React. You create a store with a function that returns state and methods to update it, then consume it with a hook - no providers, no actions, no reducers required. Zustand supports both immutable updates (like Redux) and direct mutations using Immer under the hood, giving you flexibility in how you write your code.
Performance-wise, both libraries are excellent. Redux requires React-Redux's Provider wrapper and selector functions to optimize re-renders, while Zustand components automatically subscribe to only the state slices they use without a provider. Zustand's bundle size is about 90% smaller than Redux + React-Redux, which matters for performance-conscious applications.
Redux shines when you need its mature ecosystem: Redux DevTools with time-travel debugging, extensive middleware options (Saga, Thunk, Observable), and established patterns that make large codebases predictable. Zustand is perfect for modern projects that prioritize developer experience, minimal boilerplate, and don't need Redux's heavyweight tooling. Many teams are migrating from Redux to Zustand for new features while maintaining Redux for legacy code.
Code Example:
// Redux Implementation with Redux Toolkit
import { configureStore, createSlice } from '@reduxjs/toolkit';
import { Provider, useSelector, useDispatch } from 'react-redux';
// Define slice with reducers
const bearSlice = createSlice({
name: 'bears',
initialState: {
count: 0,
lastFed: null,
hunger: 0
},
reducers: {
increasePopulation: (state) => {
state.count += 1; // Redux Toolkit uses Immer internally
},
removeAllBears: (state) => {
state.count = 0;
},
feedBears: (state) => {
state.lastFed = new Date().toISOString();
state.hunger = 0;
},
increaseHunger: (state) => {
state.hunger += 1;
}
}
});
// Export actions
export const { increasePopulation, removeAllBears, feedBears, increaseHunger } =
bearSlice.actions;
// Configure store
const store = configureStore({
reducer: {
bears: bearSlice.reducer
}
});
// Must wrap app with Provider
function App() {
return (
<Provider store={store}>
<BearCounter />
<Controls />
</Provider>
);
}
// Component using Redux - requires hooks and dispatch
function BearCounter() {
// Selector to get specific state slice
const count = useSelector(state => state.bears.count);
const hunger = useSelector(state => state.bears.hunger);
return (
<div>
<h1>{count} bears</h1>
<p>Hunger level: {hunger}</p>
</div>
);
}
function Controls() {
const dispatch = useDispatch(); // Get dispatch function
return (
<div>
<button onClick={() => dispatch(increasePopulation())}>Add bear</button>
<button onClick={() => dispatch(removeAllBears())}>Remove all</button>
<button onClick={() => dispatch(feedBears())}>Feed bears</button>
</div>
);
}
// Zustand Implementation - Much Simpler
import { create } from 'zustand';
// Create store with state and actions in one place
const useBearStore = create((set) => ({
// State
count: 0,
lastFed: null,
hunger: 0,
// Actions - just functions that call set()
increasePopulation: () => set((state) => ({ count: state.count + 1 })),
removeAllBears: () => set({ count: 0 }),
feedBears: () => set({ lastFed: new Date().toISOString(), hunger: 0 }),
increaseHunger: () => set((state) => ({ hunger: state.hunger + 1 }))
}));
// No Provider needed - just use the hook
function App() {
return (
<>
<BearCounter />
<Controls />
</>
);
}
// Component using Zustand - direct hook usage
function BearCounter() {
// Automatically subscribes to only the selected state
const count = useBearStore(state => state.count);
const hunger = useBearStore(state => state.hunger);
return (
<div>
<h1>{count} bears</h1>
<p>Hunger level: {hunger}</p>
</div>
);
}
function Controls() {
// Access actions directly from the store
const increasePopulation = useBearStore(state => state.increasePopulation);
const removeAllBears = useBearStore(state => state.removeAllBears);
const feedBears = useBearStore(state => state.feedBears);
return (
<div>
<button onClick={increasePopulation}>Add bear</button>
<button onClick={removeAllBears}>Remove all</button>
<button onClick={feedBears}>Feed bears</button>
</div>
);
}
// Zustand with Immer for Mutable Updates
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
const useTaskStore = create(
immer((set) => ({
tasks: [],
// Mutable style updates - Immer handles immutability
addTask: (task) => set((state) => {
state.tasks.push(task); // Direct mutation, Immer converts to immutable
}),
toggleTask: (id) => set((state) => {
const task = state.tasks.find(t => t.id === id);
if (task) task.completed = !task.completed; // Direct mutation
}),
removeTask: (id) => set((state) => {
const index = state.tasks.findIndex(t => t.id === id);
if (index !== -1) state.tasks.splice(index, 1); // Direct array mutation
})
}))
);
// Zustand with DevTools (Redux DevTools compatible)
import { devtools } from 'zustand/middleware';
const useCounterStore = create(
devtools(
(set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 }), false, 'increment'),
decrement: () => set((state) => ({ count: state.count - 1 }), false, 'decrement')
}),
{ name: 'CounterStore' } // Name shown in DevTools
)
);
// Zustand with Async Actions (No Thunk/Saga needed)
const useUserStore = create((set, get) => ({
user: null,
loading: false,
error: null,
// Async action - just regular async function
fetchUser: async (userId) => {
set({ loading: true, error: null });
try {
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
set({ user, loading: false });
} catch (error) {
set({ error: error.message, loading: false });
}
},
// Access current state with get()
updateUserName: (name) => {
const currentUser = get().user;
set({ user: { ...currentUser, name } });
}
}));
// Zustand with Computed Values
const useCartStore = create((set, get) => ({
items: [],
addItem: (item) => set((state) => ({
items: [...state.items, item]
})),
// Computed value as a function
getTotal: () => {
return get().items.reduce((sum, item) => sum + item.price * item.quantity, 0);
},
// Or use a selector in the component
selectItemCount: (state) => state.items.reduce((sum, item) => sum + item.quantity, 0)
}));
function Cart() {
const items = useCartStore(state => state.items);
const getTotal = useCartStore(state => state.getTotal);
return (
<div>
<p>Items: {items.length}</p>
<p>Total: ${getTotal()}</p>
</div>
);
}
Comparison Table:
| Feature | Redux (with Toolkit) | Zustand |
|---|---|---|
| Bundle Size | ~10kb (Redux + React-Redux) | ~1kb |
| Setup Complexity | Medium - slices, store config | Minimal - one function |
| Boilerplate | Low with Toolkit (High with plain Redux) | Very Low |
| Provider Required | Yes | No |
| State Updates | Immutable (Immer via Toolkit) | Immutable or Mutable (with Immer middleware) |
| Actions | Required (action creators) | Optional (just functions) |
| Reducers | Required | Not needed |
| DevTools | Excellent built-in support | Available via middleware |
| Middleware | Extensive ecosystem | Built-in (immer, persist, devtools) |
| Async Handling | Thunk, Saga, Observable | Just async functions |
| Learning Curve | Medium-Steep | Gentle |
| TypeScript Support | Excellent but verbose | Excellent and concise |
| Re-render Optimization | Manual selectors | Automatic with selectors |
| Time-Travel Debugging | Yes (standard) | Yes (with devtools middleware) |
| Ecosystem Maturity | Very Mature (since 2015) | Growing (since 2019) |
| Best For | Large apps, large teams, strict patterns | Modern apps, quick setup, DX |
| Performance | Excellent with proper selectors | Excellent by default |
| Testing | Easy - pure functions | Easy - regular functions |
Mermaid Diagram:
flowchart TD
subgraph Redux["Redux Architecture"]
R1[Component] -->|dispatch action| R2[Action Creator]
R2 --> R3[Reducer]
R3 -->|updates| R4[Store]
R4 -->|useSelector| R1
R5[Provider] -->|wraps| R1
R4 -.->|connects| R5
end
subgraph Zustand["Zustand Architecture"]
Z1[Component] -->|calls action| Z2[Store Action]
Z2 -->|set function| Z3[Store State]
Z3 -->|hook selector| Z1
Z4[No Provider Needed] -.->|direct access| Z1
end
Redux -.->|Migration Path| Zustand
style R4 fill:#764ABC
style Z3 fill:#FF6B6B
style Z4 fill:#4ECDC4
References:
- Zustand Official Documentation
- Redux Official Documentation
- Zustand vs Redux - A Practical Comparison
Middleware
What is middleware in Redux and how does it work?
The 30-Second Answer: Redux middleware is a function that sits between dispatching an action and the moment it reaches the reducer, providing a third-party extension point to intercept, modify, or act on actions. It enables powerful capabilities like logging, async operations, routing, and more by forming a pipeline that each action passes through before reaching the reducers.
The 2-Minute Answer (If They Want More): Middleware in Redux provides a way to extend Redux's capabilities by intercepting actions between the dispatch and the reducer. Think of it as a customizable pipeline where each piece of middleware can inspect, modify, delay, or even stop actions from reaching the reducer.
When you dispatch an action, it doesn't go directly to the reducer. Instead, it passes through each middleware function in sequence. Each middleware can perform side effects (like API calls or logging), dispatch additional actions, or transform the current action before passing it along. This middleware chain is established when you create your Redux store using the applyMiddleware enhancer.
The beauty of middleware is that it follows a functional composition pattern using currying. Each middleware is a function that takes store as an argument, returns a function that takes next (the next middleware in the chain), and returns a function that takes action. This nested structure allows middleware to access the store's dispatch and getState methods, control the flow to the next middleware, and process actions.
Common use cases include logging state changes, crash reporting, handling asynchronous operations (Redux Thunk, Redux Saga), routing, and analytics tracking. Middleware keeps your action creators pure while enabling powerful side effects in a predictable, testable way.
Code Example:
// Simple logging middleware example
const loggerMiddleware = (store) => (next) => (action) => {
console.log('Dispatching action:', action);
console.log('Previous state:', store.getState());
// Pass the action to the next middleware (or reducer if this is the last)
const result = next(action);
console.log('Next state:', store.getState());
return result;
};
// Middleware that blocks certain actions
const blocklistMiddleware = (store) => (next) => (action) => {
const blockedTypes = ['DANGEROUS_ACTION', 'FORBIDDEN_TYPE'];
if (blockedTypes.includes(action.type)) {
console.warn('Blocked action:', action.type);
return; // Don't call next(), action stops here
}
return next(action);
};
// Setting up the store with middleware
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
const store = createStore(
rootReducer,
applyMiddleware(
loggerMiddleware,
blocklistMiddleware,
thunk // Third-party middleware
)
);
// When you dispatch an action, it flows through the middleware chain:
store.dispatch({ type: 'USER_LOGIN', payload: { userId: 123 } });
// 1. loggerMiddleware (logs action and state)
// 2. blocklistMiddleware (checks if allowed)
// 3. thunk (handles async if needed)
// 4. Finally reaches the reducer
Mermaid Diagram:
flowchart TD
A[Action Dispatched] --> B[Middleware 1]
B --> C[Middleware 2]
C --> D[Middleware 3]
D --> E[Reducer]
E --> F[Store Updated]
F --> G[UI Re-renders]
B -.->|Can dispatch new actions| A
C -.->|Can dispatch new actions| A
D -.->|Can dispatch new actions| A
style A fill:#e1f5ff
style E fill:#ffe1e1
style F fill:#e1ffe1
References:
↑ Back to topWhat is Redux Thunk and how does it work?
The 30-Second Answer:
Redux Thunk is middleware that allows you to write action creators that return functions instead of plain action objects, enabling asynchronous logic like API calls. The returned function receives dispatch and getState as arguments, allowing you to dispatch multiple actions and access current state within async operations.
The 2-Minute Answer (If They Want More):
Redux Thunk is the most popular and simplest middleware for handling asynchronous operations in Redux. By default, Redux only accepts plain objects as actions, but Thunk extends this to accept functions. When you dispatch a function, Thunk intercepts it, executes it, and passes dispatch and getState as arguments, giving you full control over when and what to dispatch.
The typical pattern involves dispatching a "pending" action before an async operation, making the API call or performing other async work, then dispatching either a "fulfilled" action with the data or a "rejected" action with the error. This allows your UI to show loading states, handle errors gracefully, and update with the fetched data.
Thunk functions can be simple or complex. They can dispatch multiple actions at different times, access the current state to make decisions (like avoiding duplicate requests), and contain any JavaScript logic including conditionals, loops, and multiple async operations. Because thunks are just functions, they're easy to test by calling them with mock dispatch and getState functions.
The main advantage of Redux Thunk is its simplicity - there's no new syntax to learn beyond regular JavaScript functions and promises or async/await. However, for complex async workflows with cancellation, debouncing, or race conditions, you might need more powerful middleware like Redux Saga or Redux Observable.
Code Example:
// Basic thunk action creator
const fetchUser = (userId) => {
// This function is the "thunk" - it gets called by the thunk middleware
return async (dispatch, getState) => {
// Dispatch a pending action to show loading state
dispatch({ type: 'USER_FETCH_PENDING' });
try {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
// Dispatch success action with the data
dispatch({
type: 'USER_FETCH_FULFILLED',
payload: data
});
} catch (error) {
// Dispatch error action
dispatch({
type: 'USER_FETCH_REJECTED',
payload: error.message
});
}
};
};
// Advanced thunk: conditional dispatching based on state
const fetchUserIfNeeded = (userId) => {
return (dispatch, getState) => {
const { users } = getState();
// Check if we already have this user
if (users.byId[userId]) {
console.log('User already in cache, skipping fetch');
return Promise.resolve();
}
// Only fetch if not in cache
return dispatch(fetchUser(userId));
};
};
// Thunk with multiple sequential API calls
const createOrderWithPayment = (orderData, paymentData) => {
return async (dispatch, getState) => {
dispatch({ type: 'ORDER_CREATE_PENDING' });
try {
// First API call: create order
const orderResponse = await fetch('/api/orders', {
method: 'POST',
body: JSON.stringify(orderData)
});
const order = await orderResponse.json();
dispatch({ type: 'ORDER_CREATED', payload: order });
// Second API call: process payment
const paymentResponse = await fetch('/api/payments', {
method: 'POST',
body: JSON.stringify({
...paymentData,
orderId: order.id
})
});
const payment = await paymentResponse.json();
dispatch({ type: 'PAYMENT_PROCESSED', payload: payment });
dispatch({ type: 'ORDER_CREATE_FULFILLED', payload: { order, payment } });
} catch (error) {
dispatch({ type: 'ORDER_CREATE_REJECTED', payload: error.message });
}
};
};
// Using thunks in your components
import { useDispatch } from 'react-redux';
function UserProfile({ userId }) {
const dispatch = useDispatch();
useEffect(() => {
// Dispatch the thunk just like a regular action
dispatch(fetchUser(userId));
}, [userId, dispatch]);
// ... rest of component
}
// Setting up Redux Thunk middleware
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
const store = createStore(
rootReducer,
applyMiddleware(thunk)
);
// With Redux Toolkit (Thunk is included by default)
import { configureStore } from '@reduxjs/toolkit';
const store = configureStore({
reducer: rootReducer
// Thunk middleware is automatically added
});
Mermaid Diagram:
flowchart TD
A[dispatch fetchUser] --> B{Is it a function?}
B -->|Yes| C[Thunk Middleware]
B -->|No| H[Pass to next middleware]
C --> D[Execute function with dispatch & getState]
D --> E[dispatch PENDING]
E --> F[API Call]
F --> G{Success?}
G -->|Yes| I[dispatch FULFILLED with data]
G -->|No| J[dispatch REJECTED with error]
I --> K[Reducer updates state]
J --> K
style C fill:#e1f5ff
style F fill:#fff5e1
style K fill:#e1ffe1
References:
↑ Back to topActions and Reducers
What is the purpose of combineReducers?
The 30-Second Answer:
combineReducers is a Redux utility that combines multiple reducer functions into a single root reducer. Each reducer manages its own independent slice of the state tree, and combineReducers calls each one with the appropriate slice and action, then merges the results into a complete state object. This enables modular, maintainable code organization.
The 2-Minute Answer (If They Want More):
combineReducers solves the problem of managing complex state by implementing the "reducer composition" pattern. Instead of writing one giant reducer that handles every action and manages the entire state tree, you split your state into logical domains (users, posts, UI, etc.) and write a focused reducer for each domain.
When you call combineReducers with an object mapping state keys to reducer functions, it returns a new reducer that manages the combined state structure. On each action, this root reducer calls every child reducer with two arguments: the reducer's slice of state and the action. Each reducer can handle the action or return its current state unchanged. The root reducer then assembles all the slices back into a complete state object.
This pattern has several key benefits. Each reducer is simpler and easier to test since it only deals with its own data. Different team members can work on different reducers without conflicts. You can reuse reducers across projects or even create reducer factories for common patterns. The state shape is explicit - you can see the entire structure by looking at the combineReducers call.
Important to note: combineReducers enforces that each reducer only receives its own slice of state, not the entire state tree. If you need a reducer to access other slices of state, you'll need to use a different composition approach or keep a reference to shared data in multiple slices.
Code Example:
import { combineReducers, createStore } from 'redux';
// Individual domain reducers
function usersReducer(state = { currentUser: null, list: [] }, action) {
switch (action.type) {
case 'users/login':
return { ...state, currentUser: action.payload };
case 'users/logout':
return { ...state, currentUser: null };
case 'users/listLoaded':
return { ...state, list: action.payload };
default:
return state;
}
}
function postsReducer(state = { items: [], loading: false }, action) {
switch (action.type) {
case 'posts/loadingStarted':
return { ...state, loading: true };
case 'posts/loaded':
return { items: action.payload, loading: false };
case 'posts/postAdded':
return { ...state, items: [...state.items, action.payload] };
default:
return state;
}
}
function settingsReducer(state = { theme: 'light', language: 'en' }, action) {
switch (action.type) {
case 'settings/themeChanged':
return { ...state, theme: action.payload };
case 'settings/languageChanged':
return { ...state, language: action.payload };
default:
return state;
}
}
// Combine them into root reducer
const rootReducer = combineReducers({
users: usersReducer, // manages state.users
posts: postsReducer, // manages state.posts
settings: settingsReducer // manages state.settings
});
// Create store with combined reducer
const store = createStore(rootReducer);
// Resulting state shape:
// {
// users: { currentUser: null, list: [] },
// posts: { items: [], loading: false },
// settings: { theme: 'light', language: 'en' }
// }
// When you dispatch an action, ALL reducers receive it
store.dispatch({ type: 'users/login', payload: { id: 1, name: 'John' } });
// - usersReducer handles it and updates users slice
// - postsReducer receives it but returns unchanged state
// - settingsReducer receives it but returns unchanged state
// Nested combination for complex apps
const uiReducer = combineReducers({
modal: modalReducer,
notifications: notificationsReducer,
sidebar: sidebarReducer
});
const entitiesReducer = combineReducers({
users: usersReducer,
posts: postsReducer,
comments: commentsReducer
});
const appReducer = combineReducers({
entities: entitiesReducer,
ui: uiReducer,
settings: settingsReducer
});
// Resulting nested state:
// {
// entities: {
// users: {...},
// posts: {...},
// comments: {...}
// },
// ui: {
// modal: {...},
// notifications: {...},
// sidebar: {...}
// },
// settings: {...}
// }
// Manual implementation (for understanding)
function myCombineReducers(reducers) {
const reducerKeys = Object.keys(reducers);
return function combination(state = {}, action) {
const nextState = {};
let hasChanged = false;
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i];
const reducer = reducers[key];
const previousStateForKey = state[key];
const nextStateForKey = reducer(previousStateForKey, action);
nextState[key] = nextStateForKey;
hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
}
// Return new object only if something changed
return hasChanged ? nextState : state;
};
}
Mermaid Diagram:
flowchart TD
A[Action Dispatched] --> B[Root Reducer<br/>combineReducers]
B --> C[usersReducer]
B --> D[postsReducer]
B --> E[settingsReducer]
C --> C1[state.users]
D --> D1[state.posts]
E --> E1[state.settings]
C1 --> F[Merge Results]
D1 --> F
E1 --> F
F --> G[Complete State Object]
G --> H[Store Updated]
style B fill:#9C27B0,color:#fff
style F fill:#4CAF50,color:#fff
style G fill:#2196F3,color:#fff
References:
- Redux: combineReducers API
- Redux: Structuring Reducers - Using combineReducers
- Redux: Beyond combineReducers
What is reducer composition and why is it important?
The 30-Second Answer:
Reducer composition is the pattern of splitting reducer logic into smaller, focused functions where each manages its own slice of state. Instead of one massive reducer handling everything, you compose multiple reducers together, typically using combineReducers. This makes code more maintainable, testable, and easier to reason about.
The 2-Minute Answer (If They Want More): Reducer composition is a fundamental Redux pattern that applies functional programming principles to state management. The core idea is that complex reducer logic should be broken down into smaller, single-responsibility reducers that each manage a specific portion of the state tree.
The most common form is using combineReducers to split your root reducer into domain-specific reducers (users, posts, comments, etc.). Each reducer becomes responsible for its own slice of state and doesn't need to know about the overall state structure. This separation makes your code easier to understand - when debugging user-related issues, you only need to look at the users reducer.
Beyond combineReducers, you can compose reducers at any level. A complex slice might have helper functions that handle specific update logic (like adding an item to an array or updating a nested object). You can also compose reducers to share logic across different slices or to handle cross-cutting concerns. This modular approach scales well - as your application grows, you can add new reducers without modifying existing ones, following the Open/Closed Principle.
Code Example:
// Individual slice reducers - each manages its own domain
function usersReducer(state = { byId: {}, allIds: [] }, action) {
switch (action.type) {
case 'users/userAdded':
return {
byId: { ...state.byId, [action.payload.id]: action.payload },
allIds: [...state.allIds, action.payload.id]
};
default:
return state;
}
}
function postsReducer(state = { byId: {}, allIds: [] }, action) {
switch (action.type) {
case 'posts/postAdded':
return {
byId: { ...state.byId, [action.payload.id]: action.payload },
allIds: [...state.allIds, action.payload.id]
};
default:
return state;
}
}
function uiReducer(state = { loading: false, error: null }, action) {
switch (action.type) {
case 'ui/loadingStarted':
return { ...state, loading: true, error: null };
case 'ui/loadingFailed':
return { ...state, loading: false, error: action.payload };
default:
return state;
}
}
// Compose them together
import { combineReducers } from 'redux';
const rootReducer = combineReducers({
users: usersReducer,
posts: postsReducer,
ui: uiReducer
});
// Resulting state shape:
// {
// users: { byId: {}, allIds: [] },
// posts: { byId: {}, allIds: [] },
// ui: { loading: false, error: null }
// }
// Advanced composition - helper function for reusable logic
function createEntityReducer(entityName) {
const initialState = { byId: {}, allIds: [] };
return function(state = initialState, action) {
switch (action.type) {
case `${entityName}/itemAdded`:
return {
byId: { ...state.byId, [action.payload.id]: action.payload },
allIds: [...state.allIds, action.payload.id]
};
case `${entityName}/itemRemoved`:
const { [action.payload.id]: removed, ...remainingById } = state.byId;
return {
byId: remainingById,
allIds: state.allIds.filter(id => id !== action.payload.id)
};
default:
return state;
}
};
}
// Reuse the pattern
const rootReducer2 = combineReducers({
users: createEntityReducer('users'),
posts: createEntityReducer('posts'),
comments: createEntityReducer('comments')
});
Mermaid Diagram:
flowchart TD
A[Root Reducer] --> B[Users Reducer]
A --> C[Posts Reducer]
A --> D[UI Reducer]
B --> B1[users.byId]
B --> B2[users.allIds]
C --> C1[posts.byId]
C --> C2[posts.allIds]
D --> D1[ui.loading]
D --> D2[ui.error]
style A fill:#9C27B0,color:#fff
style B fill:#4CAF50,color:#fff
style C fill:#4CAF50,color:#fff
style D fill:#4CAF50,color:#fff
References:
- Redux: Structuring Reducers - Reducer Composition
- Redux: combineReducers
- Redux: Reusing Reducer Logic
State Management
What is state normalization and why is it important?
The 30-Second Answer: State normalization is the practice of structuring Redux state like a database, with entities stored by ID in flat lookup tables rather than nested structures. It prevents data duplication, makes updates faster and simpler, and eliminates sync issues when the same data appears in multiple places.
The 2-Minute Answer (If They Want More): When you fetch data from an API, it often comes nested - a user object might contain an array of posts, each post containing comments, etc. Storing this directly in Redux creates problems: updating a single comment requires navigating through users, finding the right post, then the right comment. If the same post appears in multiple places, you must update all copies.
Normalization solves this by flattening the structure. Instead of nesting, you store each entity type in its own lookup table (object) indexed by ID. Relationships are maintained through ID references, just like a relational database. This means each piece of data exists in exactly one place, making updates trivial and preventing inconsistencies.
The Redux team recommends this approach for any non-trivial application. Libraries like normalizr automate the conversion of nested API responses into normalized state, while Redux Toolkit's createEntityAdapter provides utilities for working with normalized data.
The benefits compound as your app grows: selectors become more reusable, components can access any entity directly without traversing a tree, and performance improves because updates don't require deep cloning of nested structures.
Code Example:
// ❌ Denormalized (nested) - difficult to update
const denormalizedState = {
posts: [
{
id: 1,
title: "Redux Tips",
author: {
id: 42,
name: "Jane",
avatar: "jane.jpg"
},
comments: [
{ id: 100, text: "Great post!", author: { id: 42, name: "Jane" } }
]
}
]
};
// âś… Normalized - easy to update, no duplication
const normalizedState = {
users: {
byId: {
42: { id: 42, name: "Jane", avatar: "jane.jpg" }
},
allIds: [42]
},
posts: {
byId: {
1: { id: 1, title: "Redux Tips", authorId: 42, commentIds: [100] }
},
allIds: [1]
},
comments: {
byId: {
100: { id: 100, text: "Great post!", authorId: 42, postId: 1 }
},
allIds: [100]
}
};
// Updating Jane's name is now a single operation
// denormalized.users.byId[42].name = "Jane Smith"
// vs traversing every post and comment to find all Jane references
Mermaid Diagram:
flowchart LR
subgraph Denormalized["❌ Denormalized State"]
P1["Post 1<br/>author: {Jane...}<br/>comments: [{Jane...}]"]
P2["Post 2<br/>author: {Jane...}"]
end
subgraph Normalized["âś… Normalized State"]
U[Users<br/>42: Jane]
PO[Posts<br/>1: authorId:42<br/>2: authorId:42]
C[Comments<br/>100: authorId:42]
PO -.authorId.-> U
C -.authorId.-> U
end
style Denormalized fill:#fee
style Normalized fill:#efe
References:
- Redux Style Guide - Normalize State Shape
- Redux Essentials - Normalizing State
- normalizr Library Documentation
Redux Toolkit
What is Redux Toolkit and why was it created?
The 30-Second Answer: Redux Toolkit is the official, opinionated, batteries-included toolset for efficient Redux development. It was created to address three common complaints about Redux: too much boilerplate code, too many packages to configure, and too much complexity for common use cases.
The 2-Minute Answer (If They Want More): Redux Toolkit (RTK) was introduced by the Redux team in 2019 to simplify Redux development and incorporate best practices by default. Before RTK, developers had to manually configure the store, write action types and action creators separately, install and set up middleware like redux-thunk, and use libraries like Immer for immutable updates.
RTK bundles these common dependencies and provides utility functions like configureStore, createSlice, and createAsyncThunk that eliminate boilerplate. It includes Redux DevTools Extension integration by default, comes with redux-thunk pre-configured, and uses Immer internally to allow "mutating" syntax while maintaining immutability.
The Redux team now recommends RTK as the standard way to write Redux logic. It doesn't change Redux fundamentals—you're still dispatching actions and updating state with reducers—but it makes the developer experience significantly better. RTK is production-ready and used by companies of all sizes.
Code Example:
// Before Redux Toolkit - Traditional Redux
const INCREMENT = 'counter/increment';
const DECREMENT = 'counter/decrement';
const increment = () => ({ type: INCREMENT });
const decrement = () => ({ type: DECREMENT });
const 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;
}
};
// After Redux Toolkit - Same logic, much less code
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => {
state.value += 1; // Looks like mutation, but Immer handles immutability
},
decrement: (state) => {
state.value -= 1;
}
}
});
export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;
Mermaid Diagram (if helpful for visualization):
flowchart TD
A[Redux Pain Points] --> B[Too Much Boilerplate]
A --> C[Complex Configuration]
A --> D[Multiple Packages]
B --> E[Redux Toolkit]
C --> E
D --> E
E --> F[configureStore]
E --> G[createSlice]
E --> H[createAsyncThunk]
E --> I[createEntityAdapter]
F --> J[Simplified Redux Dev]
G --> J
H --> J
I --> J
References:
- Redux Toolkit Official Documentation
- Redux Toolkit: Overview and Getting Started
- Why Redux Toolkit is How To Use Redux Today
Async Operations
What is the loading/success/error pattern for async actions?
The 30-Second Answer: The loading/success/error pattern is a standard way to track async operation states in Redux. You dispatch three action types: one when the request starts (loading), one when it succeeds (success with data), and one when it fails (error with message), allowing the UI to show loading spinners, display data, or show error messages.
The 2-Minute Answer (If They Want More): This pattern is fundamental to handling async operations in Redux applications. It provides a clear structure for managing the three possible states of any async operation: in-progress, successful, or failed.
When an async operation begins, you dispatch a loading/pending action that sets a loading flag to true and typically clears any previous errors. This lets your UI show loading indicators like spinners or skeleton screens. When the operation succeeds, you dispatch a success action with the received data, setting loading to false and storing the data. If it fails, you dispatch an error action with the error message, setting loading to false and storing the error for display.
This pattern is so common that Redux Toolkit's createAsyncThunk automatically generates these three action types (pending, fulfilled, rejected) for you. The pattern also extends to storing additional metadata like lastUpdated timestamps, request IDs for deduplication, or pagination info.
The key benefit is predictable state management: your UI always knows whether to show a loader, display data, or show an error message, and you can handle race conditions or multiple simultaneous requests consistently.
Code Example:
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
// Async thunk automatically creates three action types:
// - 'posts/fetch/pending'
// - 'posts/fetch/fulfilled'
// - 'posts/fetch/rejected'
const fetchPosts = createAsyncThunk('posts/fetch', async () => {
const response = await fetch('/api/posts');
return response.json();
});
const postsSlice = createSlice({
name: 'posts',
initialState: {
items: [],
loading: false,
error: null,
lastUpdated: null
},
reducers: {},
extraReducers: (builder) => {
builder
// LOADING state: Request started
.addCase(fetchPosts.pending, (state) => {
state.loading = true;
state.error = null; // Clear previous errors
})
// SUCCESS state: Request completed successfully
.addCase(fetchPosts.fulfilled, (state, action) => {
state.loading = false;
state.items = action.payload;
state.lastUpdated = Date.now();
})
// ERROR state: Request failed
.addCase(fetchPosts.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
});
}
});
// Manual pattern without Redux Toolkit
const postsReducer = (state = initialState, action) => {
switch (action.type) {
case 'posts/fetchRequest':
return { ...state, loading: true, error: null };
case 'posts/fetchSuccess':
return {
...state,
loading: false,
items: action.payload,
lastUpdated: Date.now()
};
case 'posts/fetchFailure':
return { ...state, loading: false, error: action.payload };
default:
return state;
}
};
// Component usage - show different UI based on state
function PostsList() {
const { items, loading, error } = useSelector(state => state.posts);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchPosts());
}, [dispatch]);
if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage error={error} />;
return <PostsGrid posts={items} />;
}
Mermaid Diagram:
stateDiagram-v2
[*] --> Idle
Idle --> Loading: Dispatch Request
Loading --> Success: API Returns Data
Loading --> Error: API Returns Error
Success --> Loading: Refetch
Error --> Loading: Retry
Success --> Idle
Error --> Idle
References:
- Redux Toolkit Async Logic
- Redux Style Guide - Model Actions as Events
- Loading State Pattern Best Practices
Selectors
What is a selector in Redux?
The 30-Second Answer: A selector is a function that extracts and derives specific pieces of data from the Redux store state. Selectors encapsulate the logic of reading from the state tree, allowing components to access data without knowing the exact shape of the state.
The 2-Minute Answer (If They Want More): Selectors are pure functions that take the Redux state as an argument and return some derived or extracted data. They serve as the "read API" for your Redux store, providing a clean abstraction layer between your components and the state structure.
The primary benefits of using selectors include encapsulation (components don't need to know the state shape), reusability (the same selector can be used across multiple components), and composability (complex selectors can be built from simpler ones). When you refactor your state structure, you only need to update the selectors rather than every component that accesses that data.
Selectors can be as simple as accessing a single property or as complex as computing derived data, filtering arrays, or combining multiple pieces of state. They can also be memoized to prevent unnecessary recalculations, which is particularly important for expensive computations or when used in components that re-render frequently.
Modern Redux Toolkit encourages defining selectors alongside your slice reducers, making it easy to maintain a consistent API for reading and writing state.
Code Example:
// Simple selector - directly accesses state
const selectUser = (state) => state.user;
// Deriving selector - extracts nested data
const selectUserName = (state) => state.user.profile.name;
// Computing selector - derives new data
const selectUserFullName = (state) => {
const { firstName, lastName } = state.user;
return `${firstName} ${lastName}`;
};
// Filtering selector - computes filtered results
const selectActiveUsers = (state) => {
return state.users.filter(user => user.isActive);
};
// Using selectors in a component
import { useSelector } from 'react-redux';
function UserProfile() {
// Access state via selectors
const userName = useSelector(selectUserName);
const fullName = useSelector(selectUserFullName);
return <div>{fullName}</div>;
}
// Defining selectors in a slice (Redux Toolkit)
const userSlice = createSlice({
name: 'user',
initialState: { firstName: '', lastName: '', email: '' },
reducers: { /* ... */ },
});
// Export selectors alongside actions
export const selectUserEmail = (state) => state.user.email;
export const selectUserFullName = (state) =>
`${state.user.firstName} ${state.user.lastName}`;
Mermaid Diagram (if helpful for visualization):
flowchart TD
A[Redux Store State] --> B[Selector Function]
B --> C{Type of Selector}
C -->|Simple| D[Direct Property Access]
C -->|Deriving| E[Extract Nested Data]
C -->|Computing| F[Calculate Derived Values]
C -->|Filtering| G[Filter/Transform Arrays]
D --> H[Component Receives Data]
E --> H
F --> H
G --> H
style A fill:#f9f,stroke:#333,stroke-width:2px
style H fill:#9f9,stroke:#333,stroke-width:2px
References:
- Redux Style Guide - Use Selector Functions to Read from Store State
- React Redux - useSelector Hook
- Redux Toolkit - createSlice
Performance and Testing
What is Redux DevTools and how do you use it?
The 30-Second Answer:
Redux DevTools is a browser extension that lets you inspect every action and state change, time-travel debug by jumping to previous states, and replay actions. It integrates automatically with Redux Toolkit's configureStore and provides features like action filtering, state diffing, and exporting/importing state for debugging.
The 2-Minute Answer (If They Want More): Redux DevTools is an essential debugging tool that provides deep visibility into your Redux application. It displays a log of every dispatched action, shows state before and after each action, and allows time-travel debugging—meaning you can "rewind" your application to any previous state to understand how bugs occurred.
The DevTools extension is available for Chrome, Firefox, and other browsers. When using Redux Toolkit's configureStore, DevTools integration is enabled by default with no setup required. For legacy Redux, you need to add the DevTools enhancer manually. The extension shows up in your browser's developer tools as a "Redux" tab.
Key features include: the action log showing all dispatched actions chronologically, the state tree displaying current state in an inspectable format, diff view highlighting what changed between states, the dispatcher for manually firing actions, and the ability to export/import state snapshots for reproducing bugs. You can also skip actions, lock state to prevent updates, or persist state across page refreshes.
Advanced features include trace mode (showing stack traces for actions), action filtering to focus on specific action types, custom action sanitizers to hide sensitive data, and the ability to monitor multiple stores. For production, you should disable DevTools or configure them to sanitize sensitive information, as they can expose application state.
Code Example:
// Basic setup with Redux Toolkit (DevTools enabled by default)
import { configureStore } from '@reduxjs/toolkit';
import userReducer from './userSlice';
import postsReducer from './postsSlice';
const store = configureStore({
reducer: {
user: userReducer,
posts: postsReducer
}
// DevTools automatically enabled in development
});
// Customizing DevTools options
const store = configureStore({
reducer: {
user: userReducer,
posts: postsReducer
},
devTools: process.env.NODE_ENV !== 'production' && {
name: 'MyApp',
trace: true, // Enable stack traces
traceLimit: 25,
// Sanitize sensitive data
actionSanitizer: (action) => {
if (action.type === 'user/login/fulfilled') {
return { ...action, payload: '<<SANITIZED>>' };
}
return action;
},
stateSanitizer: (state) => ({
...state,
user: state.user ? { ...state.user, password: '<<HIDDEN>>' } : null
})
}
});
// Legacy Redux setup (manual integration)
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';
const composeEnhancers =
(typeof window !== 'undefined' &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose;
const store = createStore(
rootReducer,
composeEnhancers(applyMiddleware(thunk))
);
// Advanced: Multiple store instances with custom names
const store1 = configureStore({
reducer: rootReducer1,
devTools: { name: 'Store 1 - Main App' }
});
const store2 = configureStore({
reducer: rootReducer2,
devTools: { name: 'Store 2 - Admin Panel' }
});
// Using DevTools API programmatically (for testing)
import { configureStore } from '@reduxjs/toolkit';
const store = configureStore({
reducer: rootReducer
});
// Programmatically subscribe to DevTools
const unsubscribe = store.subscribe(() => {
console.log('State updated:', store.getState());
});
// Example: Logging specific actions for debugging
const loggerMiddleware = (storeAPI) => (next) => (action) => {
console.group(action.type);
console.log('Dispatching:', action);
console.log('Previous state:', storeAPI.getState());
const result = next(action);
console.log('Next state:', storeAPI.getState());
console.groupEnd();
return result;
};
const storeWithLogging = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(loggerMiddleware)
});
// Example: Using action creators with DevTools for debugging
import { createAction } from '@reduxjs/toolkit';
// Actions show up with descriptive names in DevTools
const userLoggedIn = createAction('user/loggedIn', (user) => ({
payload: user,
meta: { timestamp: Date.now() }
}));
// Dispatching in your app
store.dispatch(userLoggedIn({ id: 1, name: 'John' }));
// DevTools will show:
// Action: user/loggedIn
// Payload: { id: 1, name: 'John' }
// Meta: { timestamp: 1703510400000 }
// Example: Exporting state for bug reports
function exportStateForBugReport() {
const state = store.getState();
const sanitized = {
...state,
user: { ...state.user, email: '<<REDACTED>>' }
};
console.log('Copy this state for bug report:');
console.log(JSON.stringify(sanitized, null, 2));
// Or download as file
const blob = new Blob([JSON.stringify(sanitized, null, 2)], {
type: 'application/json'
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `redux-state-${Date.now()}.json`;
a.click();
}
// Example: Importing state for debugging (in DevTools console)
function loadStateFromFile(jsonString) {
const state = JSON.parse(jsonString);
// Use DevTools import feature or dispatch action
store.dispatch({ type: '__LOAD_STATE__', payload: state });
}
// Common DevTools usage patterns in development:
// 1. Jump to action: Click any action in the log to see that exact state
// 2. Skip action: Right-click and "Skip" to see app without that action
// 3. Export/Import: Save state snapshot, reload page, import to reproduce
// 4. Dispatch: Use the dispatcher tab to manually fire test actions
// 5. Diff view: See exactly what changed between states
// 6. Trace: Enable to see component stack traces for each action
// Useful keyboard shortcuts in DevTools:
// - Ctrl/Cmd + H: Toggle DevTools visibility
// - Ctrl/Cmd + Q: Toggle dispatcher
// - ↑/↓: Navigate through action history
Mermaid Diagram:
flowchart TD
A[User Interaction] --> B[Dispatch Action]
B --> C[Redux Store]
C --> D[DevTools Extension]
D --> E[Action Log]
D --> F[State Inspector]
D --> G[Diff Viewer]
D --> H[Time Travel]
E --> I[Filter by type]
E --> J[Search actions]
F --> K[Browse state tree]
F --> L[Export state]
G --> M[See what changed]
H --> N[Jump to state]
H --> O[Skip actions]
H --> P[Replay actions]
C --> Q[Reducers]
Q --> R[New State]
R --> C
R --> D
style D fill:#4CAF50
style H fill:#2196F3
style E fill:#FFC107
style F fill:#FFC107
style G fill:#FFC107
References:
- Redux DevTools Extension
- Redux Toolkit: Configuring DevTools
- Redux DevTools: Features and Walkthrough