# How the Redux Subscribe/Unsubscribe Mechanism Works: A Deep Dive into Store Listeners

> Understand the Redux subscribe unsubscribe mechanism. Learn how Redux uses immutable snapshots and listener maps to safely notify subscribers during dispatch cycles. Optimize your state management.

- Repository: [Redux/redux](https://github.com/reduxjs/redux)
- Tags: deep-dive
- Published: 2026-03-05

---

**Redux implements a copy-on-write subscription pattern where listeners are stored in a `Map` called `nextListeners`, and each dispatch operation iterates over an immutable snapshot to prevent mutation errors during notification cycles.**

The subscribe/unsubscribe mechanism in Redux forms the backbone of reactive state management in the `reduxjs/redux` repository. When you call `store.subscribe(listener)`, the store registers your callback to be executed after every successful dispatch, while the returned unsubscribe function provides a safe cleanup mechanism that respects the store's internal dispatching state.

## The Core Architecture of Redux's Subscription System

Redux maintains listener state using two critical data structures in [`src/createStore.ts`](https://github.com/reduxjs/redux/blob/main/src/createStore.ts): `currentListeners` and `nextListeners`. Understanding how these interact explains why Redux can safely handle subscriptions and unsubscriptions even while notifying listeners.

### The nextListeners Map and Listener IDs

When you subscribe to the store, Redux assigns your callback a unique numeric identifier (`listenerId`) and stores it in a `Map` named `nextListeners`. This design choice ensures O(1) insertion and deletion performance while maintaining insertion order for deterministic notification sequences.

The listener registration occurs in the `subscribe` function implementation (lines 211-252 in [`src/createStore.ts`](https://github.com/reduxjs/redux/blob/main/src/createStore.ts)), where the store validates your input before mutating the listener collection.

### Copy-on-Write Safety with ensureCanMutateNextListeners

Redux implements a defensive **copy-on-write** pattern through the `ensureCanMutateNextListeners()` helper function. Before adding or removing any listener, Redux checks if `nextListeners` currently references the same object as `currentListeners`. If they match, Redux creates a shallow copy of the map to ensure that any active dispatch iteration (which uses `currentListeners`) remains unaffected by structural changes.

This mechanism prevents the "concurrent modification" bugs that would occur if a listener tried to subscribe or unsubscribe another listener during the notification phase.

## How store.subscribe() Registers Listeners

The `subscribe` method in [`src/createStore.ts`](https://github.com/reduxjs/redux/blob/main/src/createStore.ts) follows a strict validation and registration protocol to maintain store integrity.

First, Redux performs **safety checks**. It throws an error if the provided `listener` is not a function, ensuring type safety at runtime. It also verifies that `isDispatching` is false, preventing subscription attempts during active reducer execution—a guard that protects against inconsistent state observations.

Once validated, Redux invokes `ensureCanMutateNextListeners()` to obtain a safe copy of the listener map, then registers the new callback with a unique ID. The method returns an **unsubscribe closure** that captures the `listenerId` and the store's internal state, allowing for later removal without exposing internal implementation details.

## How the Unsubscribe Function Removes Listeners

When you invoke the function returned by `store.subscribe()`, you trigger the unsubscription logic defined in the closure created during subscription.

The unsubscribe function first validates that the listener is still registered (checking if the `listenerId` exists in `nextListeners`) and that no dispatch is currently in progress. Like the subscription process, it refuses to mutate the listener list during active dispatching to prevent iteration errors.

After passing these guards, the function calls `ensureCanMutateNextListeners()` to create a safe copy of the map, then deletes the listener entry using its stored ID. It also clears the `currentListeners` reference, forcing the next dispatch to rebuild a fresh snapshot from the updated `nextListeners` map.

## Dispatching with Listener Snapshots

The dispatch mechanism in Redux relies on the snapshot pattern to ensure consistent notification behavior. When `store.dispatch(action)` executes, it captures the current state of listeners using the assignment:

```typescript
const listeners = (currentListeners = nextListeners);

```

This line atomically updates `currentListeners` to reference the current `nextListeners` map, then assigns that reference to a local `listeners` variable for iteration. Because `currentListeners` now points to this specific snapshot, any subsequent subscribe or unsubscribe operations will trigger `ensureCanMutateNextListeners()` to clone the map, leaving the active `listeners` reference untouched.

The dispatch process then iterates through this snapshot:

```typescript
listeners.forEach(listener => listener());

```

This approach guarantees that listeners added during the dispatch will not be called in the current cycle, and listeners removed during the dispatch will still be called (since they existed in the snapshot), preventing undefined behavior or skipped notifications.

## Practical Code Examples

### Basic Subscription and Unsubscription

The following example demonstrates the fundamental pattern for reacting to state changes in a Redux application:

```javascript
import { createStore } from 'redux';
import rootReducer from './reducers';

const store = createStore(rootReducer);

// Register a listener that logs state changes
const unsubscribe = store.subscribe(() => {
  console.log('State updated:', store.getState());
});

// Dispatch actions - listener will fire after each
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'ADD_TODO', text: 'Learn Redux' });

// Clean up subscription when component unmounts or logic completes
unsubscribe();

```

### Selective State Change Listening

Since Redux notifies all listeners on every dispatch regardless of which state slice changed, you typically implement selective reactions by comparing previous and current values:

```javascript
function selectCount(state) {
  return state.counter;
}

let previousCount = selectCount(store.getState());

const unsubscribe = store.subscribe(() => {
  const currentCount = selectCount(store.getState());
  
  if (previousCount !== currentCount) {
    console.log(`Counter changed: ${previousCount} → ${currentCount}`);
    previousCount = currentCount;
  }
});

```

### Safety Violation: Subscribing During Dispatch

Attempting to subscribe while a reducer is executing throws an error, as shown in this anti-pattern:

```javascript
function badReducer(state = {}, action) {
  if (action.type === 'ATTEMPT_SUBSCRIBE') {
    // ERROR: Cannot subscribe while dispatching
    store.subscribe(() => console.log('This will throw'));
  }
  return state;
}

const store = createStore(badReducer);

// This dispatch will throw because the reducer tries to subscribe
store.dispatch({ type: 'ATTEMPT_SUBSCRIBE' });

```

## Summary

- Redux maintains listeners in a `Map` called `nextListeners` and assigns each a unique numeric ID for O(1) operations.
- The **copy-on-write** pattern via `ensureCanMutateNextListeners()` creates shallow copies before mutations, ensuring that active dispatch iterations remain unaffected by subscription changes.
- The `subscribe()` method in [`src/createStore.ts`](https://github.com/reduxjs/redux/blob/main/src/createStore.ts) validates that the listener is a function and that no dispatch is active before registration.
- The returned **unsubscribe function** captures the listener ID and safely removes the callback by checking dispatch state and cloning the listener map before deletion.
- During `dispatch()`, Redux captures a snapshot of `nextListeners` into `currentListeners`, iterating over this immutable reference to prevent concurrent modification errors while allowing listeners to subscribe/unsubscribe others during notification.

## Frequently Asked Questions

### What happens if I try to subscribe while a dispatch is in progress?

Redux throws an error immediately. The `subscribe` method checks the `isDispatching` flag and throws if true, preventing subscription attempts during reducer execution. This safety guard ensures that the listener list remains stable while the store is processing an action and notifying existing listeners.

### Can I unsubscribe from within a listener callback?

Yes, you can safely call the unsubscribe function from within a listener. Because Redux uses the copy-on-write snapshot pattern, the current dispatch iteration iterates over `currentListeners` (the snapshot taken at dispatch start). Your unsubscribe call will clone `nextListeners` and remove your listener from the map, but this affects only future dispatches, not the current notification cycle.

### Why does Redux use a Map instead of an array for listeners?

Redux uses a `Map` with numeric keys (listener IDs) to achieve O(1) time complexity for both subscriptions and unsubscriptions. Arrays would require O(n) operations for deletion or searching. The Map also preserves insertion order, ensuring that listeners are notified in the sequence they were added, which provides deterministic behavior for application side effects.

### Where is the subscribe logic implemented in the Redux source code?

The core implementation resides in [`src/createStore.ts`](https://github.com/reduxjs/redux/blob/main/src/createStore.ts) (specifically lines 211-252 in the current master branch). This file contains the `subscribe` function, the `ensureCanMutateNextListeners` helper, and the unsubscribe closure logic. Type definitions for the `Store.subscribe` method and the `Unsubscribe` return type are located in [`src/types/store.ts`](https://github.com/reduxjs/redux/blob/main/src/types/store.ts).