connect().subscribe: calling unsubscribe twice removes another listener

Open
#2,137 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

@unknownhackerworld is already working on this.

Since Oct 26, 2026.

  • #2138 by @unknownhackerworld — open

Assessment

Difficulty
1/5
Estimated time
Under an hour
Newbie friendliness
25/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
react, redux, typescript
Domain
frontend, testing

Research direction

Read extension/src/pageScript/api/index.ts, especially subscribe and the listener setup in connect(). The issue describes a reproduction and the expected fix: repeated calls to the returned unsubscribe function should leave other listeners intact. The author says a pull request with tests is ready, so check for existing work before taking this on.

Written by the indexing model from the issue text.

Description

Bug

Calling the unsubscribe function returned by connect().subscribe() a second time removes a different listener. Depending on what is registered, that is either another subscriber's listener or the extension's own internal listener for the connection, which then stops reacting to pause messages from the monitor.

Cause

subscribe in extension/src/pageScript/api/index.ts:

return function unsubscribe() {
  const index = listenersForId.indexOf(liftedListener);
  listenersForId.splice(index, 1);
};

After the first call the listener is no longer in the array, so indexOf returns -1. Array.prototype.splice(-1, 1) treats -1 as "the last element" and removes whatever listener is at the end of listeners[id].

listeners[id] starts as [rootListener] (set in connect()), and every subscribe() call pushes onto it. So:

  • With two subscribers [rootListener, a, b], calling a's unsubscribe twice removes b. b stops receiving monitor messages (DISPATCH, START, IMPORT, ...).
  • With one subscriber [rootListener, a], calling a's unsubscribe twice removes rootListener. That listener handles PAUSE_RECORDING (and START/STOP when autoPause is on), so the pause button in the monitor no longer reaches the page and no LIFTED reply is sent.

Calling an unsubscribe more than once is easy to hit from app code, for example a React effect cleanup that also runs on a manual disconnect, or a library that unsubscribes defensively. Redux's own store.subscribe treats a repeated unsubscribe as a no-op.

Reproduction

const devTools = window.__REDUX_DEVTOOLS_EXTENSION__.connect({ name: 'demo' });
const unsubscribeA = devTools.subscribe((m) => console.log('a', m.type));
devTools.subscribe((m) => console.log('b', m.type));

unsubscribeA();
unsubscribeA();

Now dispatch from the monitor (for example the "Dispatcher" or start/pause recording). Nothing is logged for b, although b never unsubscribed.

Suggested fix

Only call splice when the listener was found (index !== -1), so a repeated call is a no-op. I have a small PR with tests ready.

Dominant language
TypeScript
Stars
14.4k
Forks
1.2k
Avg merge
3d 6h
Merged PRs (30d)
12

Getting set up

This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from reduxjs/redux-devtools

All issues in reduxjs/redux-devtools

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.