From 143b1c5a6b4dd6e25f1213f98e1945bb9f345015 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Tue, 11 Aug 2026 18:26:37 +0200 Subject: [PATCH] refactor: remove Link transition state --- .changeset/remove-link-transition-state.md | 7 +++ packages/react-router/src/link.tsx | 19 +----- packages/solid-router/src/link.tsx | 20 +----- packages/vue-router/src/link.tsx | 32 +--------- packages/vue-router/tests/link.test.tsx | 73 ---------------------- 5 files changed, 11 insertions(+), 140 deletions(-) create mode 100644 .changeset/remove-link-transition-state.md diff --git a/.changeset/remove-link-transition-state.md b/.changeset/remove-link-transition-state.md new file mode 100644 index 0000000000..c9f7b9d341 --- /dev/null +++ b/.changeset/remove-link-transition-state.md @@ -0,0 +1,7 @@ +--- +'@tanstack/react-router': patch +'@tanstack/solid-router': patch +'@tanstack/vue-router': patch +--- + +remove the undocumented Link `isTransitioning` state and `data-transitioning` attribute diff --git a/packages/react-router/src/link.tsx b/packages/react-router/src/link.tsx index fc33e6b594..73a9c00570 100644 --- a/packages/react-router/src/link.tsx +++ b/packages/react-router/src/link.tsx @@ -2,7 +2,6 @@ import * as React from 'react' import { useStore } from '@tanstack/react-store' -import { flushSync } from 'react-dom' import { deepEqual, exactPathTest, @@ -592,8 +591,6 @@ export function useLinkProps< ...resolvedInactiveProps.style, } - // eslint-disable-next-line react-hooks/rules-of-hooks - const [isTransitioning, setIsTransitioning] = React.useState(false) // eslint-disable-next-line react-hooks/rules-of-hooks const hasRenderFetched = React.useRef(false) @@ -660,15 +657,6 @@ export function useLinkProps< ) { e.preventDefault() - flushSync(() => { - setIsTransitioning(true) - }) - - const unsub = router.subscribe('onResolved', () => { - unsub() - setIsTransitioning(false) - }) - // All is well? Navigate! // N.B. we don't call `router.commitLocation(next) here because we want to run `validateSearch` before committing router.navigate({ @@ -756,7 +744,6 @@ export function useLinkProps< ...(resolvedClassName && { className: resolvedClassName }), ...(disabled && STATIC_DISABLED_PROPS), ...(isActive && STATIC_ACTIVE_PROPS), - ...(isHydrated && isTransitioning && STATIC_TRANSITIONING_PROPS), } } @@ -764,7 +751,6 @@ const STATIC_EMPTY_OBJECT = {} const STATIC_ACTIVE_OBJECT = { className: 'active' } const STATIC_DISABLED_PROPS = { role: 'link', 'aria-disabled': true } const STATIC_ACTIVE_PROPS = { 'data-status': 'active', 'aria-current': 'page' } -const STATIC_TRANSITIONING_PROPS = { 'data-transitioning': 'transitioning' } const timeoutMap = new WeakMap>() @@ -865,10 +851,7 @@ export interface LinkPropsChildren { // If a function is passed as a child, it will be given the `isActive` boolean to aid in further styling on the element it returns children?: | React.ReactNode - | ((state: { - isActive: boolean - isTransitioning: boolean - }) => React.ReactNode) + | ((state: { isActive: boolean }) => React.ReactNode) } type LinkComponentReactProps = Omit< diff --git a/packages/solid-router/src/link.tsx b/packages/solid-router/src/link.tsx index 0b2d101d40..edd6018121 100644 --- a/packages/solid-router/src/link.tsx +++ b/packages/solid-router/src/link.tsx @@ -43,7 +43,6 @@ export function useLinkProps< options: UseLinkPropsOptions, ): Solid.ComponentProps<'a'> { const router = useRouter() - const [isTransitioning, setIsTransitioning] = Solid.createSignal(false) const shouldHydrateHash = !isServer && !!router.options.ssr const hasHydrated = useHydrated() @@ -324,13 +323,6 @@ export function useLinkProps< ) { e.preventDefault() - setIsTransitioning(true) - - const unsub = router.subscribe('onResolved', () => { - unsub() - setIsTransitioning(false) - }) - // All is well? Navigate! // N.B. we don't call `router.commitLocation(next) here because we want to run `validateSearch` before committing router.navigate({ @@ -435,7 +427,6 @@ export function useLinkProps< disabled: !!local.disabled, target: local.target, ...(local.disabled && STATIC_DISABLED_PROPS), - ...(isTransitioning() && STATIC_TRANSITIONING_ATTRIBUTES), } if (simpleStyling()) { @@ -490,9 +481,6 @@ const STATIC_ACTIVE_ATTRIBUTES = { 'data-status': 'active', 'aria-current': 'page', } -const STATIC_TRANSITIONING_ATTRIBUTES = { - 'data-transitioning': 'transitioning', -} /** Call a JSX.EventHandlerUnion with the event. */ function callHandler( @@ -569,10 +557,7 @@ export interface LinkPropsChildren { // If a function is passed as a child, it will be given the `isActive` boolean to aid in further styling on the element it returns children?: | Solid.JSX.Element - | ((state: { - isActive: boolean - isTransitioning: boolean - }) => Solid.JSX.Element) + | ((state: { isActive: boolean }) => Solid.JSX.Element) } type LinkComponentSolidProps = TComp extends Solid.ValidComponent @@ -655,9 +640,6 @@ export const Link: LinkComponent<'a'> = (props) => { get isActive() { return (linkProps as any)['data-status'] === 'active' }, - get isTransitioning() { - return (linkProps as any)['data-transitioning'] === 'transitioning' - }, }) } diff --git a/packages/vue-router/src/link.tsx b/packages/vue-router/src/link.tsx index 75c1901e4e..56a9efe650 100644 --- a/packages/vue-router/src/link.tsx +++ b/packages/vue-router/src/link.tsx @@ -86,7 +86,6 @@ export function useLinkProps< options: UseLinkPropsOptions, ): LinkHTMLAttributes { const router = useRouter() - const isTransitioning = Vue.ref(false) let hasRenderFetched = false // Ensure router is defined before proceeding @@ -200,7 +199,6 @@ export function useLinkProps< href, options: options as AnyLinkPropsOptions, isActive, - isTransitioning: false, resolvedActiveProps, resolvedInactiveProps, resolvedClassName, @@ -299,13 +297,6 @@ export function useLinkProps< e.preventDefault() - isTransitioning.value = true - - const unsub = router.subscribe('onResolved', () => { - unsub() - isTransitioning.value = false - }) - // All is well? Navigate! router.navigate({ ...options, @@ -423,7 +414,6 @@ export function useLinkProps< ref, staticEventHandlers, isActive: isActive.value, - isTransitioning: isTransitioning.value, resolvedActiveProps, resolvedInactiveProps, resolvedClassName, @@ -486,7 +476,6 @@ function combineResultProps({ href, options, isActive, - isTransitioning, resolvedActiveProps, resolvedInactiveProps, resolvedClassName, @@ -498,7 +487,6 @@ function combineResultProps({ href: string | undefined options: AnyLinkPropsOptions isActive: boolean - isTransitioning: boolean resolvedActiveProps: StyledProps resolvedInactiveProps: StyledProps resolvedClassName?: string @@ -542,10 +530,6 @@ function combineResultProps({ result['aria-current'] = 'page' } - if (isTransitioning) { - result['data-transitioning'] = 'transitioning' - } - for (const key of Object.keys(resolvedActiveProps)) { if (key !== 'class' && key !== 'style') { result[key] = resolvedActiveProps[key] @@ -753,12 +737,7 @@ export type LinkProps< export interface LinkPropsChildren { // If a function is passed as a child, it will be given the `isActive` boolean to aid in further styling on the element it returns - children?: - | Vue.VNodeChild - | ((state: { - isActive: boolean - isTransitioning: boolean - }) => Vue.VNodeChild) + children?: Vue.VNodeChild | ((state: { isActive: boolean }) => Vue.VNodeChild) } type LinkComponentVueProps = TComp extends keyof HTMLElementTagNameMap @@ -871,16 +850,9 @@ const LinkImpl = Vue.defineComponent({ const linkProps = Vue.unref(linkPropsSource) const isActive = linkProps['data-status'] === 'active' - const isTransitioning = - linkProps['data-transitioning'] === 'transitioning' // Create the slot content or empty array if no default slot - const slotContent = slots.default - ? slots.default({ - isActive, - isTransitioning, - }) - : [] + const slotContent = slots.default ? slots.default({ isActive }) : [] // Special handling for SVG links - wrap an inside the SVG if (Component === 'svg') { diff --git a/packages/vue-router/tests/link.test.tsx b/packages/vue-router/tests/link.test.tsx index 8d9e4a67ac..0ff6838ed0 100644 --- a/packages/vue-router/tests/link.test.tsx +++ b/packages/vue-router/tests/link.test.tsx @@ -5318,79 +5318,6 @@ describe('Link', () => { expect(window.location.pathname).toBe('/posts') }) - test('Link slot receives isTransitioning during pending navigation', async () => { - let resolvePostsLoader: (() => void) | undefined - - const postsLoaderPromise = new Promise((resolve) => { - resolvePostsLoader = resolve - }) - - const rootRoute = createRootRoute({ - component: () => - Vue.h(Vue.Fragment, null, [ - Vue.h( - Link, - { to: '/posts', 'data-testid': 'posts-link' }, - { - default: ({ isTransitioning }: { isTransitioning: boolean }) => ( - <> - - {isTransitioning ? 'transitioning' : 'idle'} - - Posts - - ), - }, - ), - Vue.h(Outlet), - ]), - }) - - const indexRoute = createRoute({ - getParentRoute: () => rootRoute, - path: '/', - component: () =>

Index page

, - }) - - const postsRoute = createRoute({ - ssr: false, - getParentRoute: () => rootRoute, - path: '/posts', - loader: () => postsLoaderPromise, - component: () =>

Posts page

, - }) - - const router = createRouter({ - routeTree: rootRoute.addChildren([indexRoute, postsRoute]), - history, - }) - - render() - - await screen.findByRole('heading', { name: 'Index page' }) - - const postsLink = await screen.findByTestId('posts-link') - const transitionState = await screen.findByTestId('slot-transition-state') - - expect(transitionState).toHaveTextContent('idle') - expect(postsLink).not.toHaveAttribute('data-transitioning') - - fireEvent.click(postsLink) - - await waitFor(() => expect(resolvePostsLoader).toBeDefined()) - await waitFor(() => - expect(transitionState).toHaveTextContent('transitioning'), - ) - expect(postsLink).toHaveAttribute('data-transitioning', 'transitioning') - - resolvePostsLoader?.() - - await screen.findByRole('heading', { name: 'Posts page' }) - - await waitFor(() => expect(transitionState).toHaveTextContent('idle')) - expect(postsLink).not.toHaveAttribute('data-transitioning') - }) - describe('when preloading a link, `preload` should be', () => { async function runTest({ expectedPreload,