Skip to content

Background stale reload result is discarded when a navigation starts from a useEffect of the newly rendered route #8644

Description

@fossamagna

Which project does this relate to?

Router

Describe the bug

This issue was drafted with the help of an AI assistant (Claude) and reviewed by me.

A route with the default staleReloadMode: 'background' is re-entered with cached data. The router renders the cached data and starts the background reload. If the newly rendered route navigates again from a useEffect (e.g. to remove a one-shot search param such as ?saved=true), the background reload result is thrown away. The loader runs, but its result is never applied. The stale data stays on screen. It is not reloaded later either, because the follow-up navigation keeps the same match.

  • Last good: @tanstack/react-router@1.170.18 (router-core@1.171.15)
  • First bad: @tanstack/react-router@1.170.19 (router-core@1.171.16, released right after fix(router-core): lane match loader rewrite #7805)
  • Still happens on @tanstack/react-router@1.170.41 (router-core@1.171.34)

Things that do not trigger it:

  • Awaiting the navigation and then navigating again (outside the commit). The successor picks up the pending background load as expected.
  • staleReloadMode: 'blocking' on the route.

The rewritten client loader seems to drop the background work when router._tx !== tx after the commit. A navigation started from an effect during that commit replaces _tx before runBackground publishes. The successor does not reuse the pending background loader in this case. In loader-architecture-regressions.test.ts ("a superseding navigation reuses a pending background loader after async beforeLoad"), it does.

Complete minimal reproducer

https://stackblitz.com/edit/vitejs-vite-a5xhw7wa?file=package.json,src%2Fmain.tsx&terminal=dev

Steps to Reproduce the Bug

  1. Open /. It shows value: 0.
  2. Click "Edit".
  3. Click "Save". This increments the server value and navigates to /?saved=true.
  4. "Saved!" appears and the URL becomes /.
  5. Wait. It still shows value: 0.

Expected behavior

After step 5 it should show value: 1, as it does on 1.170.18. The background reload result should be applied (or handed over to the successor navigation), not dropped. Removing the useEffect navigation, or setting staleReloadMode: 'blocking', makes it show value: 1.

Screenshots or Videos

No response

Platform

  • Router / Start Version: @tanstack/react-router 1.170.41 (router-core 1.171.34). Regressed in 1.170.19 (router-core 1.171.16).
  • OS: Linux
  • Browser: Chromium (Playwright); also confirmed on StackBlitz
  • Browser Version: 149.0.7827.0
  • Bundler: vite
  • Bundler Version: 8.0.14

Additional context

We hit this in a real app: after saving on an edit page, the detail page kept showing the pre-save status. Our toast effect removes ?saved= right after mount. We switched that route to staleReloadMode: 'blocking' as a workaround.

src/main.tsx of the reproducer:

import React, { useEffect, useState } from 'react'
import ReactDOM from 'react-dom/client'
import {
  Outlet,
  RouterProvider,
  createRootRoute,
  createRoute,
  createRouter,
} from '@tanstack/react-router'

// Fake server state
let serverValue = 0
const fetchValue = async () => {
  await new Promise((r) => setTimeout(r, 200))
  return serverValue
}

const rootRoute = createRootRoute({ component: () => <Outlet /> })

const detailRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: '/',
  validateSearch: (search: Record<string, unknown>) => ({
    saved: search.saved === true ? true : undefined,
  }),
  // default staleReloadMode: 'background'
  loader: async () => ({ value: await fetchValue() }),
  component: function Detail() {
    const { value } = detailRoute.useLoaderData()
    const { saved } = detailRoute.useSearch()
    const navigate = detailRoute.useNavigate()
    const [toast, setToast] = useState(false)

    // Show a one-shot "Saved" toast, then remove `saved` from the URL.
    useEffect(() => {
      if (!saved) return
      setToast(true)
      navigate({ search: {}, replace: true })
    }, [saved, navigate])

    return (
      <div>
        <p data-testid="value">value: {value}</p>
        {toast && <p>Saved!</p>}
        <button onClick={() => navigate({ to: '/edit' })}>Edit</button>
      </div>
    )
  },
})

const editRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: '/edit',
  component: function Edit() {
    const navigate = editRoute.useNavigate()
    return (
      <button
        onClick={async () => {
          serverValue++ // mutation
          await navigate({ to: '/', search: { saved: true } })
        }}
      >
        Save
      </button>
    )
  },
})

const router = createRouter({
  routeTree: rootRoute.addChildren([detailRoute, editRoute]),
})

declare module '@tanstack/react-router' {
  interface Register {
    router: typeof router
  }
}

ReactDOM.createRoot(document.getElementById('root')!).render(
  <RouterProvider router={router} />,
)

Activity

  1. self-assigned this
    on Oct 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

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