From d96588e14acd286fc33aa719e720df2be5e4103e Mon Sep 17 00:00:00 2001 From: Andreas Turku Date: Wed, 19 Aug 2026 10:57:00 +0200 Subject: [PATCH] fix(router-core): keep a reloading match's previous beforeLoad context until it settles --- ...ue-8115-beforeload-context-window.test.tsx | 51 +++++++++++++++++++ packages/router-core/src/load-client.ts | 12 ++++- 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 packages/react-router/tests/issue-8115-beforeload-context-window.test.tsx diff --git a/packages/react-router/tests/issue-8115-beforeload-context-window.test.tsx b/packages/react-router/tests/issue-8115-beforeload-context-window.test.tsx new file mode 100644 index 0000000000..9d59d2fff6 --- /dev/null +++ b/packages/react-router/tests/issue-8115-beforeload-context-window.test.tsx @@ -0,0 +1,51 @@ +import * as React from 'react' +import { act, cleanup, render, screen } from '@testing-library/react' +import { afterEach, expect, test, vi } from 'vitest' +import { + RouterProvider, + createMemoryHistory, + createRootRoute, + createRouter, +} from '../src' + +afterEach(() => { + cleanup() + vi.useRealTimers() +}) + +// https://github.com/TanStack/router/issues/8115 +test('#8115: a reloading match keeps its previous beforeLoad context until the new result lands', async () => { + vi.useFakeTimers() + + const observed: Array = [] + let runs = 0 + const rootRoute = createRootRoute({ + beforeLoad: async ({ matches }) => { + runs++ + if (runs > 1) { + observed.push((matches[0] as any).context?.locale) + } + await new Promise((resolve) => setTimeout(resolve, 100)) + return { locale: 'en' } + }, + component: () =>
ok
, + }) + const router = createRouter({ + routeTree: rootRoute, + history: createMemoryHistory({ initialEntries: ['/'] }), + }) + + render() + await act(async () => { + await vi.advanceTimersByTimeAsync(100) + }) + expect(screen.getByTestId('content')).toBeInTheDocument() + + await act(async () => { + void router.invalidate() + await vi.advanceTimersByTimeAsync(100) + }) + + expect(runs).toBe(2) + expect(observed).toEqual(['en']) +}) diff --git a/packages/router-core/src/load-client.ts b/packages/router-core/src/load-client.ts index 71c97a63ab..a057897ba3 100644 --- a/packages/router-core/src/load-client.ts +++ b/packages/router-core/src/load-client.ts @@ -392,7 +392,13 @@ async function contextualize( ...parentContext, ...routeContext, } - match.context = context + // Seeded from the committed match so a reloading match never exposes a + // context stripped of its previous beforeLoad result (#8115). + const base = options[2 /* base */][index] + match.context = + base?.id === match.id && base.context + ? { ...base.context, ...context } + : context } catch (cause) { releaseFlight(router, match) return [index, normalizeLaneError(router, lane, route, cause, options)] @@ -402,6 +408,7 @@ async function contextualize( } const validationError = match.paramsError ?? match.searchError if (validationError !== undefined) { + match.context = context releaseFlight(router, match) return [ index, @@ -410,6 +417,7 @@ async function contextualize( } const beforeLoad = route.options.beforeLoad if (!beforeLoad) { + match.context = context continue } @@ -449,6 +457,7 @@ async function contextualize( options, ) if (outcome[0 /* kind */] !== SUCCESS) { + match.context = context releaseFlight(router, match) return [index, outcome] } @@ -457,6 +466,7 @@ async function contextualize( ...result, } } catch (cause) { + match.context = context releaseFlight(router, match) return [index, normalizeLaneError(router, lane, route, cause, options)] } finally {