Skip to content

Bug: "Duplicate declaration hot" when using two tanstackRouter() plugin instances in dev #7174

@simllll

Description

@simllll

Describe the bug

When using two tanstackRouter() plugin instances in the same Vite config (e.g., for serving two different route trees in development), an intermittent Duplicate declaration "hot" error occurs during SSR module evaluation.

Error when evaluating SSR module ../client/src/entry-server-ai.tsx: Duplicate declaration "hot"
  Plugin: tanstack-router:code-splitter:compile-reference-file
  File: /path/to/routes-ai/app/tasks.tsx

Root Cause

globalThis.TSR_ROUTES_BY_ID_MAP is shared across all plugin instances and overwritten on every generator run.

In router-generator-plugin.js:

globalThis.TSR_ROUTES_BY_ID_MAP = generator.getRoutesByFileMap();

Both generator instances write to the same global, so the last one to run wins. Meanwhile, both code-splitter instances register their own tanstack-router:code-splitter:compile-reference-file transform hook. Vite does not deduplicate plugins with the same name, so both transform hooks run on every matching file.

When a route file is transformed:

  1. Code-splitter instance Issue on "/" paths #1 transforms the file, and the react-refresh-ignored-route-exports plugin injects const hot = import.meta.hot at program scope
  2. Code-splitter instance Navigate without second parameter #2 receives the already-transformed output, sees the file in the shared TSR_ROUTES_BY_ID_MAP, transforms it again, injecting a second const hot = import.meta.hot at program scope

This causes the Babel Duplicate declaration "hot" error.

introduced with PR #7144 (ping @schiller-manuel )

Why it's intermittent

The TSR_ROUTES_BY_ID_MAP is overwritten on every generator run and watchChange event. Depending on timing:

  • If generator Navigate without second parameter #2 ran last, only its route files are in the map — so only those files get double-transformed
  • During HMR file changes, whichever generator handles the change last determines which files are in the map

Your minimal, reproducible example

Vite config with two tanstackRouter() instances, which is a common pattern for serving multiple domains/route trees from one dev server:

// vite.config.ts
import { tanstackRouter } from "@tanstack/router-plugin/vite";

const routerPlugins = [
  tanstackRouter({
    routesDirectory: "./src/routes-app",
    generatedRouteTree: "./src/routeTree-app.gen.ts",
    target: "react",
    autoCodeSplitting: true,
  }),
  tanstackRouter({
    routesDirectory: "./src/routes-ai",
    generatedRouteTree: "./src/routeTree-ai.gen.ts",
    target: "react",
    autoCodeSplitting: true,
  }),
];

Start the dev server and make a few file edits to trigger HMR. The error will appear intermittently during SSR evaluation.

Expected behavior

Each tanstackRouter() instance should only transform route files from its own routesDirectory. The TSR_ROUTES_BY_ID_MAP should be namespaced per instance (e.g., keyed by routesDirectory) rather than shared globally, and each code-splitter should only process files from its own namespace.

Suggested fix

  1. Namespace the global map — use a Map<instanceId, Map<fileId, info>> structure, or key by routesDirectory
  2. Each code-splitter checks only its own namespace — in the transform handler, filter TSR_ROUTES_BY_ID_MAP to only files belonging to this instance's routesDirectory

How Do You Currently Work Around This?

Setting codeSplittingOptions: { addHmr: false } on both instances prevents the duplicate injection, but loses TanStack Router's custom route HMR (falls back to full page reloads).

Versions

  • @tanstack/router-plugin: 1.167.20
  • @tanstack/react-router: 1.168.19
  • vite: 8.0.8
  • @vitejs/plugin-react: 6.0.1

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions