Skip to content
Open
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
7 changes: 7 additions & 0 deletions .changeset/fix-stale-params-on-navigation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@solidjs/router": patch
---

Fix params becoming undefined in outgoing components during navigation.

Route roots are now created under the `Routes` component owner so they survive re-runs of the route state memo (Solid 2 disposes roots created inside a computation when it re-runs). Each route context also gets params scoped to its own lifetime: they retain their last values while the route is being torn down, so effects and async fetches in outgoing components (e.g. kept alive by a `<Loading>` boundary) never observe another route's params. This also removes the incorrect fallback that could hand a route context another route's match during teardown.
37 changes: 25 additions & 12 deletions src/routers/components.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*@refresh skip*/

import type {Component, JSX, Owner} from "solid-js";
import {children, createMemo, createRoot, getOwner, merge, untrack} from "solid-js";
import {children, createMemo, createRoot, getOwner, merge, runWithOwner, untrack} from "solid-js";
import {getRequestEvent, isServer, type RequestEvent} from "@solidjs/web";
import {
createBranches,
Expand Down Expand Up @@ -117,6 +117,10 @@ function Routes(props: { routerState: RouterContext; branches: Branch[] }) {
const disposers: (() => void)[] = [];
let root: RouteContext | undefined;
let prevMatches: RouteMatch[] | undefined;
// Route roots must outlive re-runs of the `routeStates` memo below, so they
// are created under the owner of this component rather than the memo's
// computation (which disposes its children every time it re-runs).
const owner = getOwner()!;

const routeStates = createMemo((prev: RouteContext[] | undefined) => {
const nextMatches = props.routerState.matches();
Expand All @@ -135,18 +139,27 @@ function Routes(props: { routerState: RouterContext; branches: Branch[] }) {
disposers[i]();
}

createRoot(dispose => {
disposers[i] = dispose;
next[i] = createRouteContext(
props.routerState,
next[i - 1] || props.routerState.base,
createOutlet(() => routeStates()?.[i + 1]),
() => {
runWithOwner(owner, () =>
createRoot(dispose => {
disposers[i] = dispose;
const routeKey = nextMatch.route.key;
// Retain the last matches in which this route participated so
// that its components and preloads never observe another
// route's params/path while this route is being torn down.
const matchesAtLevel = createMemo((prev?: RouteMatch[]) => {
const routeMatches = props.routerState.matches();
return routeMatches[i] ?? routeMatches[0];
}
);
});
const m = routeMatches[i];
return m && m.route.key === routeKey ? routeMatches : prev || nextMatches;
});
next[i] = createRouteContext(
props.routerState,
next[i - 1] || props.routerState.base,
createOutlet(() => routeStates()?.[i + 1]),
() => matchesAtLevel()[i],
matchesAtLevel
);
})
);
}
}

Expand Down
39 changes: 25 additions & 14 deletions src/routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ export const useCurrentMatches = () => useRouter().matches;
* const getUser = query(() => fetchUser(params.id), "user");
* ```
*/
export const useParams = <T extends Params>() => useRouter().params as T;
export const useParams = <T extends Params>() => useRoute().params as T;

/**
* Retrieves a tuple containing a reactive object to read the current location's query parameters and a method to update them.
Expand Down Expand Up @@ -378,6 +378,14 @@ export function getRouteMatches(branches: Branch[], location: string): RouteMatc
return [];
}

function mergeParams(matches: RouteMatch[]): Params {
const params: Params = {};
for (let i = 0; i < matches.length; i++) {
Object.assign(params, matches[i].params);
}
return params;
}

function createLocation(
path: Accessor<string>,
state: Accessor<any>,
Expand Down Expand Up @@ -487,21 +495,17 @@ export function createRouterContext(
return getRouteMatches(branches(), location.pathname);
});

const buildParams = () => {
const m = matches();
const params: Params = {};
for (let i = 0; i < m.length; i++) {
Object.assign(params, m[i].params);
}
return params;
};
const buildParams = () => mergeParams(matches());

const wrapParams = utils.paramsWrapper
? (getParams: () => Params) => utils.paramsWrapper!(getParams, branches)
: (getParams: () => Params) => createMemoObject(getParams);

const params = utils.paramsWrapper
? utils.paramsWrapper(buildParams, branches)
: createMemoObject(buildParams);
const params = wrapParams(buildParams);

const baseRoute: RouteContext = {
pattern: basePath,
params,
path: () => basePath,
outlet: () => null,
resolvePath(to: string) {
Expand All @@ -513,6 +517,7 @@ export function createRouterContext(
base: baseRoute,
location,
params,
wrapParams,
isRouting,
renderPath,
parsePath,
Expand Down Expand Up @@ -673,11 +678,16 @@ export function createRouteContext(
router: RouterContext,
parent: RouteContext,
outlet: () => JSX.Element,
match: () => RouteMatch
match: () => RouteMatch,
matches: () => RouteMatch[] = () => [match()]
): RouteContext {
const { base, location, params } = router;
const { base, location, wrapParams } = router;
const { pattern, component, preload } = match().route;
const path = createMemo(() => match().path);
// Params scoped to this route's lifetime. `matches` is expected to retain
// its last valid value while this route is being torn down, so outgoing
// components and preloads never observe another route's params.
const params = wrapParams(() => mergeParams(matches()));

component &&
(component as MaybePreloadableComponent).preload &&
Expand All @@ -689,6 +699,7 @@ export function createRouteContext(
const route: RouteContext = {
parent,
pattern,
params,
path,
outlet: () =>
component
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ export interface RouteContext {
parent?: RouteContext;
child?: RouteContext;
pattern: string;
params: Params;
path: () => string;
outlet: () => JSX.Element;
resolvePath(to: string): string | undefined;
Expand All @@ -173,6 +174,7 @@ export interface RouterContext {
base: RouteContext;
location: Location;
params: Params;
wrapParams: (getParams: () => Params) => Params;
navigatorFactory: NavigatorFactory;
isRouting: () => boolean;
matches: () => RouteMatch[];
Expand Down
Loading
Loading