remix-run/react-router · error

A history only accepts one active listener

Error message

A history only accepts one active listener

What it means

A history instance supports exactly one active listener, and that slot is taken by the router itself (it subscribes via listen() to drive state updates). lib/router/history.ts:749 throws when listen() is called again before the previous listener unsubscribes, surfacing that two pieces of code think they own the same history.

Solutions

  1. Create a separate history instance for each router
  2. If you need to observe the router, use router.subscribe() or router.navigate listeners instead of history.listen()
  3. Ensure any manual listen() subscription is cleaned up (call the returned unsubscribe) before listening again
  4. Check that only one <RouterProvider> consumes a given history

Example fix

// before (shared instance, second listen throws)
const history = createBrowserHistory()
const routerA = createBrowserRouter(routes, { history })
history.listen(trackUrls)
// after
const routerA = createBrowserRouter(routes)
routerA.subscribe(trackUrls)
Defensive patterns

Strategy: validation

Validate before calling

// Before creating a second router on a shared history, verify it is unlistened
const history = createBrowserHistory();
// one router per history instance:
const router = createBrowserRouter(routes, { history });

Try / catch

try { history.listen(fn) } catch (e) { if (e instanceof Error && e.message.includes('one active listener')) { /* unsubscribe previous, then retry */ prevUnsubscribe?.(); history.listen(fn) } else throw e }

Prevention

When it happens

Trigger: Passing the same createBrowserHistory() instance to two routers (two <RouterProvider>s, or a router plus a custom listen call); calling history.listen(...) yourself on the history already given to createBrowserRouter/createRouter; listening again in an effect whose cleanup never ran.

Common situations: Sharing a module-level history singleton between a main app and a modal/secondary router; sync/DevTools code that calls history.listen for URL tracking; test setups that mount the app twice without unmounting.

Related errors


AI-assisted analysis of remix-run/react-router@917e6fdc0f (2026-08-18). Data as JSON: /api/errors/0341aaf0fad18848. Report an issue: GitHub.

Appendix: 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 917e6fdc0f)