Summary
Redux is a predictable state management library for JavaScript applications that implements the Flux architecture pattern. It provides a centralized store for application state, enabling predictable state updates through actions and pure reducer functions. Redux excels at managing complex application state with features like time-travel debugging, middleware support, and a robust ecosystem. While Redux Toolkit has modernized Redux development by reducing boilerplate, the core principles remain: single source of truth, immutable state updates, and unidirectional data flow. This cheatsheet covers Redux fundamentals, modern patterns with Redux Toolkit, and best practices for technical interviews.
Core Concepts
What is Redux?
- Predictable state container for JavaScript apps
- Based on Flux architecture and functional programming principles
- Single source of truth - entire app state in one store
- State is read-only - changes only through actions
- Pure functions (reducers) specify state transformations
Three Principles
- Single Source of Truth: One store for entire app state
- State is Read-Only: Only actions can trigger changes
- Changes via Pure Functions: Reducers must be pure functions
Basic Building Blocks
1. Store
Central state container holding the entire application state tree.
import { createStore } from 'redux';
const store = createStore(reducer);
// Store methods
store.getState() // Get current state
store.dispatch(action) // Dispatch action
store.subscribe(listener) // Subscribe to changes
2. Actions
Plain objects describing what happened. Must have a type property.
// Action
const ADD_TODO = 'ADD_TODO';
// Action Creator
const addTodo = (text) => ({
type: ADD_TODO,
payload: { text, id: Date.now() }
});
3. Reducers
Pure functions that specify how state changes in response to actions.
const initialState = { todos: [] };
const todoReducer = (state = initialState, action) => {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [...state.todos, action.payload]
};
case 'REMOVE_TODO':
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload)
};
default:
return state;
}
};
4. Data Flow
- Dispatch an action
- Store calls reducer with current state and action
- Root reducer combines output of multiple reducers
- Store saves new state tree
- Listeners are notified
Redux Toolkit (Modern Redux)
Why Redux Toolkit?
- Official recommended approach
- Reduces boilerplate
- Includes best practices by default
- Built-in Immer for immutable updates
createSlice
Combines actions and reducers in one place:
import { createSlice } from '@reduxjs/toolkit';
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
addTodo: (state, action) => {
state.push(action.payload); // Immer handles immutability
},
removeTodo: (state, action) => {
return state.filter(todo => todo.id !== action.payload);
}
}
});
export const { addTodo, removeTodo } = todosSlice.actions;
export default todosSlice.reducer;
configureStore
Simplified store setup with good defaults:
import { configureStore } from '@reduxjs/toolkit';
import todosReducer from './todosSlice';
const store = configureStore({
reducer: {
todos: todosReducer
}
});
React-Redux Integration
Provider
Makes store available to all components:
import { Provider } from 'react-redux';
function App() {
return (
<Provider store={store}>
<YourApp />
</Provider>
);
}
useSelector Hook
Extract data from Redux store:
import { useSelector } from 'react-redux';
function TodoList() {
const todos = useSelector(state => state.todos);
return <div>{/* render todos */}</div>;
}
useDispatch Hook
Dispatch actions from components:
import { useDispatch } from 'react-redux';
import { addTodo } from './todosSlice';
function AddTodo() {
const dispatch = useDispatch();
const handleAdd = (text) => {
dispatch(addTodo({ text, id: Date.now() }));
};
return <button onClick={() => handleAdd('New Todo')}>Add</button>;
}
Middleware
What is Middleware?
- Extends Redux with custom functionality
- Intercepts actions before they reach reducers
- Common uses: logging, async actions, routing
Redux Thunk
Allows action creators to return functions instead of actions:
// Async action creator with thunk
const fetchTodos = () => {
return async (dispatch, getState) => {
dispatch({ type: 'FETCH_START' });
try {
const response = await fetch('/api/todos');
const data = await response.json();
dispatch({ type: 'FETCH_SUCCESS', payload: data });
} catch (error) {
dispatch({ type: 'FETCH_ERROR', payload: error.message });
}
};
};
Custom Middleware
const loggerMiddleware = store => next => action => {
console.log('Dispatching:', action);
const result = next(action);
console.log('Next State:', store.getState());
return result;
};
Async Operations
createAsyncThunk (Redux Toolkit)
Simplified async logic:
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
const fetchUserById = createAsyncThunk(
'users/fetchById',
async (userId) => {
const response = await fetch(`/api/users/${userId}`);
return response.json();
}
);
const usersSlice = createSlice({
name: 'users',
initialState: { entities: [], loading: false },
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUserById.pending, (state) => {
state.loading = true;
})
.addCase(fetchUserById.fulfilled, (state, action) => {
state.loading = false;
state.entities.push(action.payload);
})
.addCase(fetchUserById.rejected, (state) => {
state.loading = false;
});
}
});
Performance Optimization
1. Normalized State Shape
Store data in a flat structure with IDs as keys:
// Bad
state = {
posts: [
{ id: 1, author: { id: 1, name: 'John' }, title: 'Post 1' },
{ id: 2, author: { id: 1, name: 'John' }, title: 'Post 2' }
]
}
// Good
state = {
posts: {
byId: {
1: { id: 1, authorId: 1, title: 'Post 1' },
2: { id: 2, authorId: 1, title: 'Post 2' }
},
allIds: [1, 2]
},
authors: {
byId: { 1: { id: 1, name: 'John' } },
allIds: [1]
}
}
2. Memoized Selectors (Reselect)
Prevent unnecessary re-renders:
import { createSelector } from '@reduxjs/toolkit';
const selectTodos = state => state.todos;
const selectFilter = state => state.filter;
const selectFilteredTodos = createSelector(
[selectTodos, selectFilter],
(todos, filter) => {
switch (filter) {
case 'completed':
return todos.filter(todo => todo.completed);
case 'active':
return todos.filter(todo => !todo.completed);
default:
return todos;
}
}
);
3. React.memo with useSelector
const TodoItem = React.memo(({ todoId }) => {
const todo = useSelector(state =>
state.todos.find(t => t.id === todoId)
);
return <div>{todo.text}</div>;
});
Best Practices
1. State Shape Design
- Keep state minimal and derived data out
- Normalize complex nested data
- Store UI state separately from domain data
2. Action Design
- Use descriptive action types:
todos/addTodonotADD - Keep actions minimal - compute in reducers
- Use action creators for consistency
3. Reducer Design
- Never mutate state directly (use spread operator or Immer)
- Handle unknown actions (return current state)
- Keep reducers pure - no side effects
4. Selector Patterns
- Use selectors to encapsulate state shape
- Memoize expensive computations
- Keep selectors near reducers
Key Interview Concepts
Redux vs Context API Comparison
| Feature | Redux | Context API |
|---|---|---|
| Setup Complexity | More boilerplate | Minimal setup |
| DevTools | Time-travel debugging | Basic React DevTools |
| Performance | Optimized with selectors | Can cause re-render issues |
| Middleware | Rich ecosystem | Manual implementation |
| Best For | Complex state logic | Simple state sharing |
| Learning Curve | Steeper | Gentler |
| Bundle Size | ~10KB (RTK) | Built into React |
When to Use Redux Decision Matrix
| Scenario | Use Redux? | Alternative |
|---|---|---|
| Simple prop drilling (2-3 levels) | ❌ No | Props/Composition |
| Global UI theme | ❌ No | Context API |
| Complex business logic | ✅ Yes | - |
| Multiple data sources | ✅ Yes | - |
| Undo/Redo functionality | ✅ Yes | - |
| Server state management | ⚠️ Maybe | React Query/SWR |
| Form state | ❌ No | React Hook Form |
Side Effects Management Strategies
| Solution | Use Case | Complexity | Example |
|---|---|---|---|
| Redux Thunk | Simple async | Low | API calls, timers |
| Redux Saga | Complex flows | High | Debouncing, racing |
| Redux Observable | RxJS patterns | High | Stream processing |
| RTK Query | Data fetching | Low | CRUD operations |
| Listener Middleware | Event-driven | Medium | WebSocket, SSE |
Common Redux Anti-Patterns
// ❌ Anti-Pattern: Mutating state
state.todos.push(newTodo); // NEVER do this
// ✅ Correct: Return new state
return {
...state,
todos: [...state.todos, newTodo]
};
// ❌ Anti-Pattern: Logic in components
const sortedTodos = todos.sort((a, b) => a.date - b.date);
// ✅ Correct: Use selectors
const sortedTodos = useSelector(selectSortedTodos);
// ❌ Anti-Pattern: Storing derived state
state = {
todos: [...],
completedCount: 5 // Derived from todos
};
// ✅ Correct: Compute in selectors
const completedCount = createSelector(
[selectTodos],
todos => todos.filter(t => t.completed).length
);
Redux DevTools Features
| Feature | Purpose | Keyboard Shortcut |
|---|---|---|
| Action Log | View dispatched actions | - |
| State Tree | Inspect current state | - |
| Time Travel | Jump to previous states | ← → arrows |
| Dispatch | Manually dispatch actions | - |
| Import/Export | Save/load state | - |
| Skip Action | Ignore specific actions | Click action |
| Jump | Go to specific action | Double-click |
Testing Redux
Testing Reducers
test('should handle ADD_TODO', () => {
const initialState = { todos: [] };
const action = {
type: 'ADD_TODO',
payload: { id: 1, text: 'Test' }
};
const newState = todoReducer(initialState, action);
expect(newState.todos).toHaveLength(1);
expect(newState.todos[0].text).toBe('Test');
});
Testing Connected Components
import { render } from '@testing-library/react';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
test('renders todos', () => {
const mockStore = configureStore([]);
const store = mockStore({
todos: [{ id: 1, text: 'Test Todo' }]
});
const { getByText } = render(
<Provider store={store}>
<TodoList />
</Provider>
);
expect(getByText('Test Todo')).toBeInTheDocument();
});
RTK Query (Data Fetching)
Modern data fetching solution in Redux Toolkit:
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: (builder) => ({
getTodos: builder.query({
query: () => 'todos'
}),
addTodo: builder.mutation({
query: (todo) => ({
url: 'todos',
method: 'POST',
body: todo
})
})
})
});
// Auto-generated hooks
export const { useGetTodosQuery, useAddTodoMutation } = api;
Advanced Patterns
Optimistic Updates
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
addTodoOptimistic: (state, action) => {
state.push({ ...action.payload, pending: true });
},
confirmTodo: (state, action) => {
const todo = state.find(t => t.tempId === action.payload.tempId);
if (todo) {
todo.id = action.payload.id;
todo.pending = false;
delete todo.tempId;
}
},
removeTodoOptimistic: (state, action) => {
return state.filter(t => t.tempId !== action.payload);
}
}
});
Undo/Redo Implementation
const undoableReducer = (reducer) => {
const initialState = {
past: [],
present: reducer(undefined, {}),
future: []
};
return (state = initialState, action) => {
switch (action.type) {
case 'UNDO':
if (state.past.length === 0) return state;
return {
past: state.past.slice(0, -1),
present: state.past[state.past.length - 1],
future: [state.present, ...state.future]
};
case 'REDO':
if (state.future.length === 0) return state;
return {
past: [...state.past, state.present],
present: state.future[0],
future: state.future.slice(1)
};
default:
const newPresent = reducer(state.present, action);
if (newPresent === state.present) return state;
return {
past: [...state.past, state.present],
present: newPresent,
future: []
};
}
};
};
Entity Adapter Pattern (RTK)
import { createEntityAdapter } from '@reduxjs/toolkit';
const todosAdapter = createEntityAdapter({
selectId: (todo) => todo.id,
sortComparer: (a, b) => a.title.localeCompare(b.title)
});
const todosSlice = createSlice({
name: 'todos',
initialState: todosAdapter.getInitialState(),
reducers: {
addOne: todosAdapter.addOne,
addMany: todosAdapter.addMany,
updateOne: todosAdapter.updateOne,
removeOne: todosAdapter.removeOne,
setAll: todosAdapter.setAll
}
});
// Generated selectors
const todosSelectors = todosAdapter.getSelectors(
(state) => state.todos
);
Common Pitfalls & Solutions
State Mutation Detection
// Development only - Redux Toolkit includes this by default
import { createStore } from 'redux';
import { produce } from 'immer';
const store = createStore(
reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__({
trace: true,
traceLimit: 25
})
);
Memory Leaks Prevention
// Cleanup subscriptions
useEffect(() => {
const unsubscribe = store.subscribe(() => {
// Handle state changes
});
return () => unsubscribe(); // Cleanup
}, []);
Performance Metrics
Selector Performance
// Measure selector performance
const selectExpensiveData = createSelector(
[selectAllItems, selectFilters],
(items, filters) => {
console.time('Expensive Selector');
const result = /* expensive computation */;
console.timeEnd('Expensive Selector');
return result;
}
);
Interview Preparation Checklist
- ✅ Core Concepts: Store, Actions, Reducers, Data flow
- ✅ Redux Toolkit: createSlice, configureStore, createAsyncThunk
- ✅ React Integration: Provider, useSelector, useDispatch
- ✅ Middleware: Thunk, custom middleware, applyMiddleware
- ✅ Async Patterns: Thunks, createAsyncThunk, RTK Query
- ✅ Performance: Normalization, memoization, reselect
- ✅ Testing: Reducer tests, integration tests, mocking store
- ✅ Best Practices: Immutability, action naming, state shape
- ✅ DevTools: Time-travel, action replay, state export
- ✅ Advanced: Undo/redo, optimistic updates, entity adapters
Key Takeaway: Redux provides predictable state management through immutable updates and unidirectional data flow. Modern Redux with Redux Toolkit significantly reduces boilerplate while maintaining the benefits of centralized state management. Focus on understanding the core principles, when to use Redux versus alternatives, and performance optimization techniques through normalization and memoized selectors.