Solid Router route-scoped accessors can throw during navigation teardown
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 15.1k
- Forks
- 1.9k
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 143
Description
Which project does this relate to?
Router
Describe the bug
In Solid 2.0 and TanStack Solid Router, route scoped accessors such as Route.useParams() and Route.useSearch() can throw after navigation has moved away from the route that created the accessor.
The test creates a route component that calls postRoute.useParams(), schedules a delayed read of that accessor, navigates away, and then lets the delayed read run. On current code, the delayed read throws:
Invariant failed: Could not find an active match from "/posts/$postId"
Obviosly you would never write such code for the actual applications, but the async reading of accessors can be triggered in createEffect() calls.
Reduced test code snippet:
component: function PostComponent() {
const params = postRoute.useParams()
// Simulated async work that reads params accessor
delayedRead = delayedReadGate.then(() => {
try {
params()
} catch (err) {
delayedError = err
}
})
return <h1>Post {params().postId}</h1>
}
await router.navigate({ to: '/other' })
// Trigger async work
releaseDelayedRead()
await delayedRead
if (delayedError) {
throw new Error(
`Route-scoped useParams threw after navigation away: ${
delayedError instanceof Error ? delayedError.message : String(delayedError)
}`,
)
}
The current behavior demonstrates that a route-created accessor can become throwing solely because active matches no longer contain that route id.
Complete minimal reproducer
https://github.com/TanStack/router/pull/7330
Steps to Reproduce the Bug
Run the added test in the pull request
CI=1 NX_DAEMON=false pnpm nx run @tanstack/solid-router:test:unit --outputStyle=stream --skipRemoteCache -- tests/useMatch.test.tsx
@tanstack/solid-router: ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯
@tanstack/solid-router: FAIL @tanstack/solid-router tests/useMatch.test.tsx > useMatch > route-scoped useParams should remain readable from async work created by the route during navigation away
@tanstack/solid-router: Error: Route-scoped useParams threw after navigation away: Invariant failed: Could not find an active match from "/posts/$postId"
@tanstack/solid-router: ❯ tests/useMatch.test.tsx:170:13
@tanstack/solid-router: 168|
@tanstack/solid-router: 169| if (delayedError) {
@tanstack/solid-router: 170| throw new Error(
@tanstack/solid-router: | ^
@tanstack/solid-router: 171| `Route-scoped useParams threw after navigation away: ${
@tanstack/solid-router: 172| delayedError instanceof Error
@tanstack/solid-router: ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
@tanstack/solid-router: Test Files 1 failed | 44 passed (45)
@tanstack/solid-router: Tests 1 failed | 802 passed | 2 skipped (805)
@tanstack/solid-router: Type Errors no errors
@tanstack/solid-router: Start at 09:26:49
@tanstack/solid-router: Duration 52.71s (transform 95.49s, setup 12.36s, import 110.95s, tests 93.44s, environment 30.98s, typecheck 3.90s)
@tanstack/solid-router: ELIFECYCLE Command failed with exit code 1.
Expected behavior
Expected behaviour here is up to debate, since route is no longer active and JavaScript does not support proper abort handling.
I have created a workaround for my application that provides params as T | undefined, this removes the errors but discards all the hard work Tanstack router has done on type safe path and search params. This is not the fix I would like to use long term.
type RegisteredRouteId = RouteIds<RegisteredRouter["routeTree"]>;
export type RouteParams<TFrom extends RegisteredRouteId> = Expand<
RouteById<RegisteredRouter["routeTree"], TFrom>["types"]["allParams"]
>;
export const useStableParams = <const TFrom extends RegisteredRouteId>(
from: TFrom,
): Accessor<RouteParams<TFrom> | undefined> =>
useParams<RegisteredRouter, TFrom, true, false, unknown>({
from,
shouldThrow: false,
});
From my perspective there are two sane options, Route.useParams() could
- return the last value instead of throwing on route changes
- support the extra api (e.g.:
Route.useParams({ shouldThrow: false })) and let the developer of the application handle
Screenshots or Videos
No response
Platform
- Router / Start Version: Router: 2.0.0-beta.17 for the router and 2.0.0-beta.18 for the start
- OS: N/A
- Browser: N/A
- Browser Version: N/A
- Bundler: N/A
- Bundler Version: N/A
Additional context
No response
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by running the added case in tests/useMatch.test.tsx with the provided pnpm nx command, then trace the implementation behind route-scoped Route.useParams() and Route.useSearch(). Confirm the desired post-navigation behavior with maintainers, since the issue presents returning the last value and allowing shouldThrow: false as alternatives; done means the test passes without losing type-safe route parameters.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100