Skip to content

Start: HMR does not update loader return values in development #6559

@ericksonholguin

Description

@ericksonholguin

Which project does this relate to?

Start

Describe the bug

Hot Module Reloading (HMR) does not reflect changes made to loader functions when their code is modified during development. Even though Vite detects the file change and logs indicate a hot update, the return value from the loader remains cached and unchanged until a full page refresh is performed.

The loader function is executed (as evidenced by console.log statements updating), but the data returned by the loader is not updated in the component that consumes it via Route.useLoaderData().

This behavior makes it extremely difficult to develop features that rely on loader data, as developers must perform full page refreshes after every change to see the updated data, defeating the purpose of HMR.

Your Example Website or App

https://github.com/ericksonholguin/Tanstack-Start-hot-reload-issues

Steps to Reproduce the Bug or Issue

  1. Create a new TanStack Start project:
pnpm create @tanstack/start@latest
  1. Add a simple loader to src/routes/index.tsx:
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/')({
  component: RouteComponent,
  loader: () => {
    console.log('Loader called')
    return {
      message: 'Original message',
    }
  },
})

function RouteComponent() {
  const data = Route.useLoaderData()
  return <div>{data.message}</div>
}
  1. Run the dev server:
pnpm dev
  1. Open the application in your browser and observe the message "Original message" being displayed.

  2. Edit the loader to return a different message:

loader: () => {
  console.log('Loader called - UPDATED')
  return {
    message: 'UPDATED message - this should show immediately',
  }
},
  1. Save the file and observe the behavior.

Observed behavior:

  • Vite logs show: [vite] (client) hmr update /src/routes/index.tsx
  • The console.log shows the updated message ("Loader called - UPDATED")
  • The UI still displays "Original message" (the old value)
  • Only after a full page refresh does the UI update to show "UPDATED message - this should show immediately"

Additional observations:

  • The loader function is being re-executed (proven by updated console.log output)
  • The return value is being cached somewhere and not invalidated during HMR
  • Using the same logic in useEffect or component state works correctly with HMR
  • Setting defaultPreloadStaleTime: 0 in the router configuration does not resolve the issue
  • Adding defaultGcTime: 0 and defaultStaleTime: 0 in development mode does not resolve the issue

Expected behavior

When a loader function is modified during development, the HMR system should:

  1. Re-execute the loader function (✅ this works)
  2. Invalidate the cached loader data
  3. Update the component with the new loader data immediately
  4. Reflect the changes in the browser without requiring a full page refresh

This is the expected behavior for any development workflow with HMR, and matches how component code, styles, and other React code behaves with Vite's HMR.

Screenshots or Videos

Image Image Image Image
Screen.Recording.2026-01-31.at.7.30.34.AM.mp4

Platform

  • Router / Start Version: 1.132.0
  • OS: macOS 26.2
  • Browser: Chrome
  • Browser Version: Version 144.0.7559.110 (Official Build) (arm64)
  • Bundler: Vite
  • Bundler Version: 7.1.7
  • Runtime: Bun v1.3.0
  • React Version: 19.2.0

Additional context

ext

Related Issues

This issue appears to be related to or a continuation of:

What we've tried

We attempted multiple solutions without success:

  1. Router configuration changes:

    • Added defaultPreloadStaleTime: 0
    • Added defaultGcTime: 0 and defaultStaleTime: 0 in dev mode
    • None of these affected the caching behavior
  2. Custom Vite plugin to invalidate modules:

    function tanstackRouterHMR(): Plugin {
      return {
        name: 'tanstack-router-hmr',
        enforce: 'post',
        handleHotUpdate(ctx) {
          if (ctx.file.includes('/routes/') && ctx.file.endsWith('.tsx')) {
            // Invalidate router and routeTree modules
            // This caused full page reloads instead of HMR updates
          }
        },
      }
    }
    • This approach caused full page reloads, not true HMR
  3. Per-route HMR invalidation:

    if (import.meta.hot) {
      import.meta.hot.accept(() => {
        import.meta.hot?.invalidate()
      })
    }
    • This caused infinite reload loops

Impact

This bug severely impacts developer experience when working with:

  • Dynamic data fetching in loaders
  • Localization/i18n data loaded via loaders
  • API response mocking during development
  • Any feature that depends on loader data

Developers are forced to perform full page refreshes constantly, which:

  • Slows down development significantly
  • Loses component state on every refresh
  • Makes iterative development frustrating

Suspected root cause

Based on our investigation, it appears that:

  1. The loader function itself is properly re-executed with HMR
  2. However, the loader's return value is cached at a layer that's not invalidated during HMR
  3. This cache might be in the SSR/hydration layer or in TanStack Router's internal cache
  4. The cache is only cleared on full page reload, not on HMR updates

We would appreciate any guidance on:

  • Whether this is a known limitation
  • If there's a workaround we haven't tried
  • If this is being actively worked on
  • What the proper fix would involve

Thank you for maintaining this excellent framework!

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions