remix-run/react-router · error · Error
A history only accepts one active listener
Error message
A history only accepts one active listener
What it means
The history implementation keeps a single `listener` slot. Calling `listen()` a second time while a previous listener is still active rejects the attempt. This is a guard against leaking listeners and double-handling popstate events.
Source
Thrown at packages/react-router/lib/router/history.ts:749
if (v5Compat && listener) {
listener({ action, location: history.location, delta: 0 });
}
}
function createURL(to: To): URL {
return createBrowserURLImpl(window, to);
}
let history: History = {
get action() {
return action;
},
get location() {
return getLocation(window, globalHistory);
},
listen(fn: Listener) {
if (listener) {
throw new Error("A history only accepts one active listener");
}
window.addEventListener(PopStateEventType, handlePop);
listener = fn;
return () => {
window.removeEventListener(PopStateEventType, handlePop);
listener = null;
};
},
createHref(to) {
return createHref(window, to);
},
createURL,
encodeLocation(to) {
// Encode a Location the same way window.location would
let url = createURL(to);
return {
pathname: url.pathname,View on GitHub (pinned to 1fd704a7da)
Solutions
- Always store and invoke the unsubscribe function returned by `listen()` before adding a new listener.
- In React effects, `return () => unlisten()` from the effect so cleanup runs before re-subscription.
- Use a single router/history instance per window; don't re-create `createBrowserHistory()` repeatedly.
- For tests, create a fresh history (or use `createMemoryHistory`) per test.
Example fix
// before const unlisten = history.listen(fnA); history.listen(fnB); // throws // after const unlistenA = history.listen(fnA); unlistenA(); const unlistenB = history.listen(fnB);
Defensive patterns
Strategy: validation
Validate before calling
let currentUnlisten: (() => void) | undefined;
function listenOnce(history: History, fn: Listener) {
currentUnlisten?.();
currentUnlisten = history.listen(fn);
return () => { currentUnlisten = undefined; currentUnlisten?.(); };
} Prevention
- Always return the unsubscribe function from React effects.
- Use one router/history instance per window.
- In tests, create a fresh memory history per case.
When it happens
Trigger: Two `history.listen(fn)` calls in a row without invoking the unsubscribe function returned by the first. Commonly happens when constructing a second `<Router>` (or calling `createBrowserRouter` again) on the same history, or when a React effect adds a listener without cleaning up the prior render's listener.
Common situations: Hot-module reloading that re-runs an effect which calls `listen` but whose cleanup didn't run; mounting two router instances against one history in tests; SSR/hydration where a listener is attached server-side then again on the client in the same JS realm.
Related errors
AI-assisted analysis of remix-run/react-router@1fd704a7da (2026-08-12).
Data as JSON: /api/errors/0341aaf0fad18848.
Report an issue: GitHub.