LearnThatStack Ace your next interview

Redux.
Interview cheat sheet.

Quick reference for Redux - sectioned for fast scanning. Skim the part you're shaky on, walk in confident.

Frontend Development 16-section reference ~9 min read

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

  1. Single Source of Truth: One store for entire app state
  2. State is Read-Only: Only actions can trigger changes
  3. 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

  1. Dispatch an action
  2. Store calls reducer with current state and action
  3. Root reducer combines output of multiple reducers
  4. Store saves new state tree
  5. 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/addTodo not ADD
  • 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.

Found this useful? Pass it on.
Pro · $10/mo

The sheet is free. Pro goes deeper.

Pro opens the full question library behind every sheet, every refresher and a monthly AI allowance. One subscription, all formats.

Full question library All refreshers Cancel anytime