Skip to content

Commit 0662562

Browse files
committed
fix(react-router): carry viewTransition on preferBack back-nav; memoize hook options
- preferBack: when the back path is taken, forward the link's `viewTransition` intent to the popstate-driven navigation (set `router.shouldViewTransition` before `history.back()`), so a `viewTransition` link behaves consistently on both its push and pop paths instead of only honoring `defaultViewTransition`. - useIsBackNavigation: memoize the navigation options on their primitive fields (mirroring `useLinkProps`) so `buildLocation` no longer re-runs on every render when callers pass a fresh inline options object. - Add a test asserting the link's `viewTransition` is set when going back.
1 parent be7f0c7 commit 0662562

3 files changed

Lines changed: 52 additions & 2 deletions

File tree

packages/react-router/src/link.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,11 @@ export function useLinkProps<
642642
// Target is the previous entry — pop instead of pushing to preserve forward
643643
// history and native scroll restoration (handled via the router's popstate).
644644
if (isBackNavigation) {
645+
// Carry the link's view-transition intent onto the popstate-driven back
646+
// navigation, which otherwise only honors `defaultViewTransition`.
647+
if (viewTransition !== undefined) {
648+
router.shouldViewTransition = viewTransition
649+
}
645650
router.history.back({ ignoreBlocker })
646651
return
647652
}

packages/react-router/src/useIsBackNavigation.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,33 @@ export function useIsBackNavigation<
7474
(prev, next) => prev.href === next.href,
7575
)
7676

77+
// Memoize on the primitive option fields (callers usually pass a fresh inline
78+
// object) so `buildLocation` below doesn't re-run every render. Mirrors the
79+
// `_options` memo in `useLinkProps`.
80+
// eslint-disable-next-line react-hooks/rules-of-hooks
81+
const _options = React.useMemo(
82+
() => options,
83+
// eslint-disable-next-line react-hooks/exhaustive-deps
84+
[
85+
router,
86+
options.from,
87+
options._fromLocation,
88+
options.hash,
89+
options.to,
90+
options.search,
91+
options.params,
92+
options.state,
93+
options.mask,
94+
options.unsafeRelative,
95+
],
96+
)
97+
7798
// eslint-disable-next-line react-hooks/rules-of-hooks
7899
return React.useMemo(() => {
79100
const next = router.buildLocation({
80101
_fromLocation: currentLocation,
81-
...options,
102+
..._options,
82103
} as any)
83104
return resolveIsBackNavigation(router, currentLocation, next, match)
84-
}, [router, currentLocation, options, match])
105+
}, [router, currentLocation, _options, match])
85106
}

packages/react-router/tests/useIsBackNavigation.test.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ function RootComponent() {
4343
<Link to="/contact" preferBack data-testid="preferback-contact">
4444
Prefer contact
4545
</Link>
46+
<Link to="/" preferBack viewTransition data-testid="preferback-home-vt">
47+
Back home (view transition)
48+
</Link>
4649
<HookProbe />
4750
<Outlet />
4851
</>
@@ -170,6 +173,27 @@ describe('Link preferBack', () => {
170173
expect(router.history.location.state.__TSR_index).toBe(2)
171174
})
172175

176+
test('carries the link\'s viewTransition intent onto the back navigation', async () => {
177+
const { router } = setup(['/'])
178+
await screen.findByText('IndexTitle')
179+
await act(() => fireEvent.click(screen.getByText('About')))
180+
await screen.findByText('AboutTitle')
181+
182+
// Capture `router.shouldViewTransition` at the moment `back()` is invoked —
183+
// the popstate-driven load consumes and resets it shortly after.
184+
const origBack = router.history.back.bind(router.history)
185+
let vtAtBack: unknown = 'unset'
186+
vi.spyOn(router.history, 'back').mockImplementation((opts) => {
187+
vtAtBack = router.shouldViewTransition
188+
return origBack(opts)
189+
})
190+
191+
await act(() => fireEvent.click(screen.getByTestId('preferback-home-vt')))
192+
await screen.findByText('IndexTitle')
193+
194+
expect(vtAtBack).toBe(true)
195+
})
196+
173197
describe('match modes (pathname vs exact)', () => {
174198
function ModeProbe() {
175199
const exactMatch = useIsBackNavigation({ to: '/about', search: { page: 3 } }, 'exact')

0 commit comments

Comments
 (0)