# Redux listenerIdCounter: How Redux Manages Subscription IDs Internally

> Discover how Redux uses listenerIdCounter to efficiently manage subscription IDs with unique numeric keys and achieve O(1) subscription management for optimal performance.

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

---

**The `listenerIdCounter` is a monotonic integer counter defined in [`src/createStore.ts`](https://github.com/reduxjs/redux/blob/main/src/createStore.ts) that generates unique numeric keys for each subscriber, storing them in a `Map<number, ListenerCallback>` to enable O(1) subscription management and consistent snapshot isolation during dispatch cycles.**

The `listenerIdCounter` serves as the backbone of Redux's subscription system in the reduxjs/redux repository. This internal utility ensures every call to `store.subscribe` receives a collision-free identifier while working in tandem with a dual-map architecture to prevent mutations during state updates.

## What Is the listenerIdCounter in Redux?

In the core store implementation, the `listenerIdCounter` is initialized as a simple numeric variable at module level in [`src/createStore.ts`](https://github.com/reduxjs/redux/blob/main/src/createStore.ts) at line 152:

```typescript
let listenerIdCounter = 0;

```

This counter increments monotonically each time a new listener subscribes to the store. When `subscribe` is invoked, the current value is captured as the key at line 232, and the counter immediately increments for the next subscription:

```typescript
const listenerId = listenerIdCounter++;
nextListeners.set(listenerId, listener);

```

Unlike random UUID generation or array indices, this approach guarantees sequential, collision-free keys while providing optimal lookup performance when listeners must be removed.

## How Redux Manages Subscriptions Using listenerIdCounter

Redux employs a sophisticated map-based storage system paired with snapshot semantics to ensure consistent behavior during dispatch cycles. The `listenerIdCounter` provides the unique keys that make this system possible.

### Unique ID Generation and Storage

Each subscription receives a numeric ID from the counter and is stored in `nextListeners`, a `Map<number, ListenerCallback>` defined at lines 150–151:

```typescript
let currentListeners: Map<number, ListenerCallback> | null = new Map();
let nextListeners = currentListeners;

```

The TypeScript definitions in [`src/types/store.ts`](https://github.com/reduxjs/redux/blob/main/src/types/store.ts) specify `ListenerCallback` as the function type and define the `Unsubscribe` interface returned by the subscribe method. The use of a `Map` rather than an array allows Redux to delete specific listeners in constant time using their `listenerId` as the key, avoiding expensive array splicing operations.

### The Dual-Map Architecture

Redux maintains two references to the listener map: `currentListeners` and `nextListeners`. Initially, both variables point to the same `Map` instance. This architecture enables snapshot isolation—listeners can subscribe or unsubscribe during a dispatch without affecting the current iteration.

The `currentListeners` variable holds the snapshot currently being executed, while `nextListeners` represents the mutable working copy that accumulates changes for the next dispatch cycle.

### Snapshot Isolation with ensureCanMutateNextListeners

Before any mutation occurs, Redux clones the listener map to prevent affecting the ongoing dispatch. The `ensureCanMutateNextListeners` function in [`src/createStore.ts`](https://github.com/reduxjs/redux/blob/main/src/createStore.ts) performs this lazy cloning:

```typescript
function ensureCanMutateNextListeners() {
  if (nextListeners === currentListeners) {
    nextListeners = new Map();
    currentListeners.forEach((listener, key) => {
      nextListeners.set(key, listener);
    });
  }
}

```

This function checks reference equality between the two maps. If they point to the same object (indicating no pending mutations exist), it creates a shallow clone. This ensures that modifications to `nextListeners` never affect the `currentListeners` snapshot during active dispatch iterations.

## The Subscription and Unsubscription Lifecycle

Understanding how `listenerIdCounter` integrates with the full lifecycle reveals how Redux maintains consistency across complex update patterns.

### Registering a New Listener

When `store.subscribe(listener)` is called, Redux executes three critical steps:

1. **Clone check**: Calls `ensureCanMutateNextListeners()` to guarantee a mutable copy exists
2. **ID assignment**: Retrieves the next available ID from `listenerIdCounter` and increments it
3. **Storage**: Inserts the listener into `nextListeners` using the numeric ID as the key

The method returns an `unsubscribe` function that retains access to the specific `listenerId` via closure, enabling precise removal without searching.

### The Unsubscribe Function and Cleanup

The `unsubscribe` function returned by `subscribe` performs several safety checks before removal. First, it verifies the listener hasn't already been unsubscribed using an internal `isSubscribed` boolean. Then it validates that the call isn't occurring during a reducer execution—throwing an error if the internal `isDispatching` flag is true to prevent inconsistent state transitions.

Upon successful validation, it calls `ensureCanMutateNextListeners()` and deletes the entry:

```typescript
nextListeners.delete(listenerId);

```

Finally, it sets `currentListeners = null`. This nullification forces the next dispatch to treat the listener collection as stale, triggering a fresh snapshot that excludes the removed listener.

### Execution During Dispatch

At the conclusion of the `dispatch` method, Redux updates the reference and notifies all listeners:

```typescript
const listeners = (currentListeners = nextListeners);
listeners.forEach(listener => {
  listener();
});

```

By assigning `currentListeners = nextListeners` before iteration, Redux atomically swaps to the latest listener collection. Any subscriptions or unsubscriptions triggered during this iteration will only affect `nextListeners`, leaving the `currentListeners` snapshot intact for the remainder of the dispatch cycle.

## Code Examples

### Basic Subscription and Unsubscription

```typescript
import { createStore } from 'redux';

function counter(state = 0, action: any) {
  switch (action.type) {
    case 'inc': return state + 1;
    default: return state;
  }
}

const store = createStore(counter);

const unsubscribe = store.subscribe(() => {
  console.log('State changed to', store.getState());
});

store.dispatch({ type: 'inc' }); // Logs: State changed to 1
unsubscribe();
store.dispatch({ type: 'inc' }); // No output

```

### Subscribing Inside a Listener (Snapshot Safety)

```typescript
let innerUnsub: (() => void) | undefined;

const outerUnsub = store.subscribe(() => {
  console.log('Outer listener runs');
  // Subscribe during dispatch - won't fire until next dispatch
  innerUnsub = store.subscribe(() => console.log('Inner listener runs'));
});

store.dispatch({ type: 'inc' });
// Output: Outer listener runs

store.dispatch({ type: 'inc' });
// Output: Outer listener runs
//         Inner listener runs

```

## Summary

- The `listenerIdCounter` in [`src/createStore.ts`](https://github.com/reduxjs/redux/blob/main/src/createStore.ts) generates sequential numeric IDs for every subscription, stored as keys in a `Map<number, ListenerCallback>`.
- Redux uses a dual-map pattern (`currentListeners` and `nextListeners`) to snapshot the subscription list during dispatches, preventing mutations from affecting active iterations.
- The `ensureCanMutateNextListeners` function lazily clones the map only when modifications are pending, optimizing performance for read-heavy operations.
- Unsubscription immediately removes the entry from `nextListeners` and nullifies `currentListeners`, ensuring the removal takes effect on the next dispatch cycle.
- Subscription changes during a dispatch never affect the current notification cycle, only future dispatches.

## Frequently Asked Questions

### Why does Redux use a counter instead of random UUIDs or array indices?

Redux uses a monotonic integer counter because it provides guaranteed unique keys with zero collision risk and minimal memory overhead. Unlike array indices—which shift during deletion and require O(n) splicing operations—the `Map` structure keyed by numeric IDs allows O(1) insertion and deletion. Random UUIDs would introduce unnecessary entropy and larger memory footprints for what is fundamentally a sequential registration process.

### What happens if I unsubscribe while a reducer is executing?

The `unsubscribe` function explicitly checks the internal `isDispatching` flag and throws an error if called during reducer execution. This safety mechanism prevents inconsistent state where a listener might miss notifications or receive partial updates. You must wait until the current dispatch cycle completes before removing listeners.

### Can I access the listenerIdCounter or the internal listener Map directly?

No, both `listenerIdCounter` and the `currentListeners`/`nextListeners` maps are private variables within the `createStore` closure. They are not exposed on the store API to prevent external mutation. The only interaction provided is through `store.subscribe()`, which returns an unsubscribe function capable of removing that specific listener.

### Why does Redux clone the listener map during subscriptions?

Redux clones the listener map via `ensureCanMutateNextListeners` to implement snapshot semantics. Without this cloning, adding or removing a listener during a dispatch—for example, if one listener subscribes another—would modify the collection currently being iterated, potentially skipping listeners or causing unexpected behavior. The copy-on-write strategy ensures the current dispatch sees a stable, immutable snapshot while future dispatches see the updated list.