Skip to content

Safari HMR Broken After Page Refresh in React Router 7.1.0+ #14101

@0xcybertim

Description

@0xcybertim

Reproduction

  1. Create a new React Router 7.6.0 project with Vite
  2. Start development server (npm run dev)
  3. Open the app in Safari desktop
  4. Make changes to a route file (e.g., app/routes/home.tsx)
  5. Observe HMR working correctly
  6. Refresh the page in Safari
  7. Notice the page loads the old cached version with hydration errors

Environment: Any React Router 7.1.0+ project with Vite dev server

System Info

System:
    OS: macOS (darwin 24.2.0)
  Binaries:
    Node: 22.14.0
    npm: 10.7.0
  Browsers:
    Safari: Affected
    Chrome: Works fine
    Firefox: Works fine
  npmPackages:
    react-router: 7.6.0
    @react-router/dev: 7.6.0
    @react-router/node: 7.6.0
    vite: 6.0.3

Used Package Manager

npm

Expected Behavior

After making changes to route files and refreshing the page in Safari:

  • Page should load the latest version of the code
  • No hydration errors should occur
  • HMR should continue working normally

Actual Behavior

After making changes to route files and refreshing the page in Safari:

  • Page loads an old cached version of the route
  • Hydration errors occur due to version mismatch
  • Development workflow is broken for Safari users

Issue Flow:

  1. Load app → ✅ Works
  2. Make changes → ✅ HMR works
  3. Make more changes → ✅ HMR continues working
  4. Refresh page → ❌ BROKEN - Loads old cached version with hydration errors

Browser Scope: Safari desktop only (Chrome/Firefox unaffected)
Affected Versions: 7.1.0 through 7.6.0+

Root Cause & Fix

React Router 7.1.0+ removed query string concatenations from route imports, breaking Safari's module cache invalidation. Safari requires query parameters to distinguish between different versions of route files.

Fix: Restore ?__react-router-build-client-route query strings in packages/react-router-dev/vite.ts at lines ~2920, ~4022, and ~3950.

Required Fixes

File: packages/react-router-dev/vite.ts (or compiled dist/vite.js)

Fix 1 - Restore routeModulePath query string (~line 2920):

let routeModulePath = combineURLs(
  ctx.publicPath,
-  `${resolveFileUrl(ctx, resolveRelativeRouteFilePath(route, ctx.reactRouterConfig))}`
+  `${resolveFileUrl(ctx, resolveRelativeRouteFilePath(route, ctx.reactRouterConfig))}?__react-router-build-client-route`
);

Fix 2 - Restore moduleUrl query string (~line 4022):

let moduleUrl = combineURLs(
  ctx.publicPath,
-  `${resolveFileUrl(ctx, resolveRelativeRouteFilePath(route, ctx.reactRouterConfig))}`
+  `${resolveFileUrl(ctx, resolveRelativeRouteFilePath(route, ctx.reactRouterConfig))}?__react-router-build-client-route`
);

Fix 3 - Fix Fast Refresh compatibility (~line 3950):

function addRefreshWrapper(reactRouterConfig, code, id) {
-  let route = getRoute(reactRouterConfig, id);
+  // Strip query string for route lookup (e.g., remove ?__react-router-build-client-route)
+  let idForRouteMatch = id.split('?')[0];
+  let route = getRoute(reactRouterConfig, idForRouteMatch);

Validation

Tested Fix: Applied manually to node_modules/@react-router/dev/dist/vite.js in React Router 7.6.0
Result: Safari HMR now works correctly after page refresh
No Regression: Chrome/Firefox continue working as expected

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions