Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/warm-routes-rest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@tanstack/solid-router': patch
'@tanstack/vue-router': patch
---

Keep active route components mounted by default when route params change.
7 changes: 5 additions & 2 deletions packages/solid-router/src/Match.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -207,13 +207,16 @@ export const MatchInner = (): any => {
const current = currentMatch()
const remount =
route.options.remountDeps ?? router.options.defaultRemountDeps
const deps = remount?.({
if (!remount) {
return routeId()
}
const deps = remount({
routeId: routeId()!,
loaderDeps: current.loaderDeps,
params: current._strictParams,
search: current._strictSearch,
})
return deps ? JSON.stringify(deps) : current.id
return JSON.stringify(deps) ?? routeId()
}

const out = () => {
Expand Down
99 changes: 99 additions & 0 deletions packages/solid-router/tests/remountDeps.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import * as Solid from 'solid-js'
import { cleanup, render, screen } from '@solidjs/testing-library'
import { afterEach, expect, test, vi } from 'vitest'
import {
Outlet,
RouterProvider,
createMemoryHistory,
createRootRoute,
createRoute,
createRouter,
} from '../src'

afterEach(() => {
cleanup()
})

function setup(remountOnParams: boolean | 'falsy' = false) {
const mounted = vi.fn()
const unmounted = vi.fn()
const rootRoute = createRootRoute({ component: () => <Outlet /> })

function ItemComponent() {
const params = itemRoute.useParams()

Solid.onMount(mounted)
Solid.onCleanup(unmounted)

return <div>Item {params().itemId}</div>
}

const itemRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/items/$itemId',
component: ItemComponent,
remountDeps:
remountOnParams === 'falsy'
? ({ params }) => (params.itemId === 'one' ? false : 0)
: remountOnParams
? ({ params }) => params
: undefined,
})
const router = createRouter({
routeTree: rootRoute.addChildren([itemRoute]),
history: createMemoryHistory({ initialEntries: ['/items/one'] }),
})

render(() => <RouterProvider router={router} />)

return { mounted, router, unmounted }
}

async function navigateToSecondItem(
router: ReturnType<typeof setup>['router'],
) {
await router.navigate({
to: '/items/$itemId',
params: { itemId: 'two' },
})
}

test('keeps an active route component mounted when params change by default', async () => {
const { mounted, router, unmounted } = setup()

expect(await screen.findByText('Item one')).toBeInTheDocument()
expect(mounted).toHaveBeenCalledTimes(1)
expect(unmounted).not.toHaveBeenCalled()

await navigateToSecondItem(router)

expect(await screen.findByText('Item two')).toBeInTheDocument()
expect(mounted).toHaveBeenCalledTimes(1)
expect(unmounted).not.toHaveBeenCalled()
})

test('remounts an active route component when params are remount deps', async () => {
const { mounted, router, unmounted } = setup(true)

expect(await screen.findByText('Item one')).toBeInTheDocument()
expect(mounted).toHaveBeenCalledTimes(1)

await navigateToSecondItem(router)

expect(await screen.findByText('Item two')).toBeInTheDocument()
expect(mounted).toHaveBeenCalledTimes(2)
expect(unmounted).toHaveBeenCalledTimes(1)
})

test('remounts when remount deps change between falsy values', async () => {
const { mounted, router, unmounted } = setup('falsy')

expect(await screen.findByText('Item one')).toBeInTheDocument()
expect(mounted).toHaveBeenCalledTimes(1)

await navigateToSecondItem(router)

expect(await screen.findByText('Item two')).toBeInTheDocument()
expect(mounted).toHaveBeenCalledTimes(2)
expect(unmounted).toHaveBeenCalledTimes(1)
})
40 changes: 15 additions & 25 deletions packages/vue-router/src/Match.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,19 +176,18 @@ export const MatchInner = Vue.defineComponent({
(router.routesById[matchRouteId] as AnyRoute).options.remountDeps ??
router.options.defaultRemountDeps

const remountDeps = remountFn
? remountFn({
routeId: matchRouteId,
loaderDeps: match.loaderDeps,
params: match._strictParams,
search: match._strictSearch,
})
const remountKey = remountFn
? JSON.stringify(
remountFn({
routeId: matchRouteId,
loaderDeps: match.loaderDeps,
params: match._strictParams,
search: match._strictSearch,
}),
)
: undefined

return [
match,
remountDeps ? JSON.stringify(remountDeps) : undefined,
] as const
return [match, remountKey] as const
})

return (): VNode | null => {
Expand Down Expand Up @@ -300,35 +299,26 @@ export const Outlet = Vue.defineComponent({

const route = router.routesById[parentRouteId]!

const childMatch = useStore(router.stores.matches, (matches) => {
const childRouteId = useStore(router.stores.matches, (matches) => {
const index = matches.findIndex(
(match) => match.routeId === parentRouteId,
)
const child = matches[index + 1]
return child
? ([
child.routeId,
child.routeId + JSON.stringify(child._strictParams),
] as const)
: undefined
return matches[index + 1]?.routeId
})

return (): VNode | null => {
if (parentMatch.value?._notFound) {
return renderRouteNotFound(router, route, parentMatch.value.error)
}

const child = childMatch.value
const child = childRouteId.value
if (!child) {
return null
}

const nextMatch = Vue.h(Match, {
routeId: child[0 /* routeId */],
// Key based on routeId + params only (not loaderDeps)
// This ensures component recreates when params change,
// but NOT when only loaderDeps change
key: child[1 /* key */],
routeId: child,
key: child,
})

// Note: We intentionally do NOT wrap in Suspense here.
Expand Down
Loading
Loading