Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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