@@ -307,12 +308,6 @@ const dashboardIndexRoute = new Route({
const invoicesRoute = new Route({
getParentRoute: () => dashboardRoute,
path: 'invoices',
- validateSearch: (search) =>
- z
- .object({
- test: z.string().optional(),
- })
- .parse(search),
component: () => {
const {
state: { data: invoices },
@@ -457,9 +452,6 @@ const invoiceRoute = new Route({
notes: z.string().optional(),
})
.parse(search),
- // getLoaderKey: ({ params, search }) => {
- // return [params.invoiceId]
- // },
loader: async ({ params: { invoiceId }, preload }) => {
const invoicesLoaderInstance = invoiceLoader.getInstance({
variables: invoiceId,
@@ -747,11 +739,12 @@ const usersIndexRoute = new Route({
const userRoute = new Route({
getParentRoute: () => usersRoute,
path: 'user',
- // parseParams: ({ userId }) => ({ userId: Number(userId) }),
- // stringifyParams: ({ userId }) => ({ userId: `${userId}` }),
validateSearch: z.object({
userId: z.number(),
}),
+ // Since our userId isn't part of our pathname, make sure we
+ // augment the userId as the key for this route
+ getKey: ({ search: { userId } }) => userId,
loader: async ({ search: { userId }, preload }) => {
const userLoaderInstance = userLoader.getInstance({ variables: userId })
@@ -797,15 +790,14 @@ const authenticatedRoute = new Route({
// If navigation is attempted while not authenticated, redirect to login
// If the error is from a prefetch, redirects will be ignored
onBeforeLoadError: (error) => {
- console.log('tanner', error)
if (error === AuthError) {
throw redirect({
to: loginRoute.to,
search: {
- // Use latestLocation (not currentLocation) to get the live url
+ // Use location (not location) to get the live url
// (as opposed to the committed url, which is technically async
// and resolved after the pending state)
- redirect: router.state.latestLocation.href,
+ redirect: router.state.location.href,
},
})
}
diff --git a/examples/react/start-basic/src/app/routes/root.tsx b/examples/react/start-basic/src/app/routes/root.tsx
index 09b3e1a976..cc20b102e6 100644
--- a/examples/react/start-basic/src/app/routes/root.tsx
+++ b/examples/react/start-basic/src/app/routes/root.tsx
@@ -21,7 +21,7 @@ export const rootRoute = RootRoute.withRouterContext
()({
component: function Root() {
const router = useRouter()
- const titleMatch = [...router.state.currentMatches]
+ const titleMatch = [...router.state.matches]
.reverse()
.find((d) => d.routeContext?.getTitle)
diff --git a/examples/react/start-kitchen-sink-single-file/src/main.tsx b/examples/react/start-kitchen-sink-single-file/src/main.tsx
index 1c38479d62..b4ed83a11c 100644
--- a/examples/react/start-kitchen-sink-single-file/src/main.tsx
+++ b/examples/react/start-kitchen-sink-single-file/src/main.tsx
@@ -784,10 +784,7 @@ const authenticatedRoute = new Route({
router.navigate({
to: loginRoute.to,
search: {
- // Use latestLocation (not currentLocation) to get the live url
- // (as opposed to the committed url, which is technically async
- // and resolved after the pending state)
- redirect: router.state.latestLocation.href,
+ redirect: router.state.location.href,
},
})
}
diff --git a/packages/react-loaders/src/index.tsx b/packages/react-loaders/src/index.tsx
index 7db8d95850..b8fa30f50d 100644
--- a/packages/react-loaders/src/index.tsx
+++ b/packages/react-loaders/src/index.tsx
@@ -91,15 +91,9 @@ LoaderInstance.onCreateFns.push((loaderInstance) => {
}
if (opts?.strict ?? true) {
- invariant(
- typeof loaderInstance.state.data !== 'undefined',
- `useLoader:
- Loader instance { key: ${loaderInstance.loader.key}, variables: ${loaderInstance.variables} }) is currently in a "${loaderInstance.state.status}" state. By default useLoader will throw an error if the loader instance is not in a "success" state. To avoid this error:
-
- - Load the loader instance before using it (e.g. via your router's loader or loader option)
-
- - Set opts.strict to false and handle the loading state in your component`,
- )
+ if (loaderInstance.state.status === 'pending') {
+ throw loaderInstance.__loadPromise || loaderInstance.load()
+ }
}
React.useEffect(() => {
diff --git a/packages/react-start/src/client.tsx b/packages/react-start/src/client.tsx
index 0fd6a01373..1414f63d83 100644
--- a/packages/react-start/src/client.tsx
+++ b/packages/react-start/src/client.tsx
@@ -24,6 +24,7 @@ export function RouterScripts() {
dangerouslySetInnerHTML={{
__html: `
window.__TSR_DEHYDRATED__ = ${JSON.stringify(dehydrated)}
+ window.__TSR_DEHYDRATED_MATCHES__ = {}
`,
}}
/>
diff --git a/packages/react-start/src/server.tsx b/packages/react-start/src/server.tsx
index 92acecd922..57e7f8be3b 100644
--- a/packages/react-start/src/server.tsx
+++ b/packages/react-start/src/server.tsx
@@ -106,11 +106,11 @@ export function StartServer(props: {
[],
)
- React.useState(() => {
- if (hydrationCtxValue) {
- props.router.hydrate(hydrationCtxValue)
- }
- })
+ // React.useState(() => {
+ // if (hydrationCtxValue) {
+ // props.router.hydrate(hydrationCtxValue)
+ // }
+ // })
return (
// Provide the hydration context still, since `` needs it.
diff --git a/packages/router-devtools/src/devtools.tsx b/packages/router-devtools/src/devtools.tsx
index 708e3fdb63..07a59a3001 100644
--- a/packages/router-devtools/src/devtools.tsx
+++ b/packages/router-devtools/src/devtools.tsx
@@ -520,17 +520,14 @@ export const TanStackRouterDevtoolsPanel = React.forwardRef<
)
const allMatches: RouteMatch[] = React.useMemo(
- () => [
- ...Object.values(router.state.currentMatches),
- ...Object.values(router.state.pendingMatches ?? []),
- ],
- [router.state.currentMatches, router.state.pendingMatches],
+ () => [...Object.values(router.state.matches)],
+ [router.state.matches],
)
const activeMatch = allMatches?.find((d) => d.route.id === activeRouteId)
const hasSearch = Object.keys(
- last(router.state.currentMatches)?.state.search || {},
+ last(router.state.matches)?.state.search || {},
).length
return (
@@ -707,7 +704,7 @@ export const TanStackRouterDevtoolsPanel = React.forwardRef<
/>
) : (
- {router.state.currentMatches.map((match, i) => {
+ {router.state.matches.map((match, i) => {
return (
)
})}
- {router.state.pendingMatches?.length ? (
- <>
-
- Pending Matches
-
- {router.state.pendingMatches?.map((match, i) => {
- return (
-
- setActiveRouteId(
- activeRouteId === match.route.id
- ? ''
- : match.route.id,
- )
- }
- style={{
- display: 'flex',
- borderBottom: `solid 1px ${theme.grayAlt}`,
- cursor: 'pointer',
- background:
- match === activeMatch
- ? 'rgba(255,255,255,.1)'
- : undefined,
- }}
- >
-
-
-
- {`${match.id}`}
-
-
- )
- })}
- >
- ) : null}
)}
@@ -953,9 +886,9 @@ export const TanStackRouterDevtoolsPanel = React.forwardRef<
}}
>
{
obj[next] = {}
return obj
diff --git a/packages/router/__tests__/index.test.tsx b/packages/router/__tests__/index.test.tsx
index 245ac3d845..20c7bf697f 100644
--- a/packages/router/__tests__/index.test.tsx
+++ b/packages/router/__tests__/index.test.tsx
@@ -55,7 +55,7 @@ function createLocation(location: Partial): ParsedLocation {
// expect(router.store.pendingMatches[0].id).toBe('/')
// await promise
-// expect(router.state.currentMatches[0].id).toBe('/')
+// expect(router.state.matches[0].id).toBe('/')
// })
// test('mounts to /a', async () => {
@@ -77,7 +77,7 @@ function createLocation(location: Partial): ParsedLocation {
// expect(router.store.pendingMatches[0].id).toBe('/a')
// await promise
-// expect(router.state.currentMatches[0].id).toBe('/a')
+// expect(router.state.matches[0].id).toBe('/a')
// })
// test('mounts to /a/b', async () => {
@@ -107,7 +107,7 @@ function createLocation(location: Partial): ParsedLocation {
// expect(router.store.pendingMatches[1].id).toBe('/a/b')
// await promise
-// expect(router.state.currentMatches[1].id).toBe('/a/b')
+// expect(router.state.matches[1].id).toBe('/a/b')
// })
// test('navigates to /a', async () => {
@@ -130,14 +130,14 @@ function createLocation(location: Partial): ParsedLocation {
// expect(router.store.pendingMatches[0].id).toBe('/')
// await promise
-// expect(router.state.currentMatches[0].id).toBe('/')
+// expect(router.state.matches[0].id).toBe('/')
// promise = router.navigate({ to: 'a' })
-// expect(router.state.currentMatches[0].id).toBe('/')
+// expect(router.state.matches[0].id).toBe('/')
// expect(router.store.pendingMatches[0].id).toBe('a')
// await promise
-// expect(router.state.currentMatches[0].id).toBe('a')
+// expect(router.state.matches[0].id).toBe('a')
// expect(router.store.pending).toBe(undefined)
// })
@@ -162,17 +162,17 @@ function createLocation(location: Partial): ParsedLocation {
// })
// await router.mount()
-// expect(router.state.currentLocation.href).toBe('/')
+// expect(router.state.location.href).toBe('/')
// let promise = router.navigate({ to: 'a' })
// expect(router.store.pendingLocation.href).toBe('/a')
// await promise
-// expect(router.state.currentLocation.href).toBe('/a')
+// expect(router.state.location.href).toBe('/a')
// promise = router.navigate({ to: './b' })
// expect(router.store.pendingLocation.href).toBe('/a/b')
// await promise
-// expect(router.state.currentLocation.href).toBe('/a/b')
+// expect(router.state.location.href).toBe('/a/b')
// expect(router.store.pending).toBe(undefined)
// })
diff --git a/packages/router/src/history.ts b/packages/router/src/history.ts
index 9a1daaf759..8486a4742f 100644
--- a/packages/router/src/history.ts
+++ b/packages/router/src/history.ts
@@ -52,7 +52,7 @@ function createHistory(opts: {
forward: () => void
createHref: (path: string) => string
}): RouterHistory {
- let currentLocation = opts.getLocation()
+ let location = opts.getLocation()
let unsub = () => {}
let listeners = new Set<() => void>()
let blockers: BlockerFn[] = []
@@ -80,13 +80,13 @@ function createHistory(opts: {
}
const onUpdate = () => {
- currentLocation = opts.getLocation()
+ location = opts.getLocation()
listeners.forEach((listener) => listener())
}
return {
get location() {
- return currentLocation
+ return location
},
listen: (cb: () => void) => {
if (listeners.size === 0) {
diff --git a/packages/router/src/path.ts b/packages/router/src/path.ts
index 210b9ae5e2..d5b74eb2c3 100644
--- a/packages/router/src/path.ts
+++ b/packages/router/src/path.ts
@@ -152,7 +152,7 @@ export function matchPathname(
matchLocation: Pick,
): AnyPathParams | undefined {
const pathParams = matchByPath(basepath, currentPathname, matchLocation)
- // const searchMatched = matchBySearch(currentLocation.search, matchLocation)
+ // const searchMatched = matchBySearch(location.search, matchLocation)
if (matchLocation.to && !pathParams) {
return
diff --git a/packages/router/src/react.tsx b/packages/router/src/react.tsx
index e0f7ca9e53..64a7698adc 100644
--- a/packages/router/src/react.tsx
+++ b/packages/router/src/react.tsx
@@ -304,6 +304,8 @@ export type RouterProps<
router: Router
}
+const useDeferredValue = React.useDeferredValue || ((d) => d)
+
export function RouterProvider<
TRouteConfig extends AnyRoute = AnyRoute,
TRoutesInfo extends AnyRoutesInfo = DefaultRoutesInfo,
@@ -311,13 +313,17 @@ export function RouterProvider<
>({ router, ...rest }: RouterProps) {
router.update(rest)
- const currentMatches = useStore(router.__store, (s) => s.currentMatches)
+ const matches = useDeferredValue(
+ useStore(router.__store, (s) => {
+ return s.matches
+ }),
+ )
React.useEffect(router.mount, [router])
return (
-
+
{
@@ -369,8 +375,9 @@ export function useMatch<
}): TStrict extends true ? TRouteMatch : TRouteMatch | undefined {
const router = useRouterContext()
const nearestMatch = useMatches()[0]!
+ const matches = useDeferredValue(router.state.matches)
const match = opts?.from
- ? router.state.currentMatches.find((d) => d.route.id === opts?.from)
+ ? matches.find((d) => d.route.id === opts?.from)
: nearestMatch
invariant(
@@ -400,22 +407,6 @@ export function useMatch<
return match as any
}
-// export function useRoute<
-// TId extends keyof RegisteredRoutesInfo['routesById'] = '/',
-// >(routeId: TId): RegisteredRoutesInfo['routesById'][TId] {
-// const router = useRouterContext()
-// const resolvedRoute = router.getRoute(routeId as any)
-
-// invariant(
-// resolvedRoute,
-// `Could not find a route for route "${
-// routeId as string
-// }"! Did you forget to add it to your route?`,
-// )
-
-// return resolvedRoute as any
-// }
-
export type RouteFromIdOrRoute = T extends RegisteredRoutesInfo['routeUnion']
? T
: T extends keyof RegisteredRoutesInfo['routesById']
@@ -424,14 +415,6 @@ export type RouteFromIdOrRoute = T extends RegisteredRoutesInfo['routeUnion']
? keyof RegisteredRoutesInfo['routesById']
: never
-// export function useRoute(
-// route: TRouteOrId extends string
-// ? keyof RegisteredRoutesInfo['routeIds']
-// : RegisteredRoutesInfo['routeUnion'],
-// ): RouteFromIdOrRoute {
-// return null as any
-// }
-
export function useLoader<
TFrom extends keyof RegisteredRoutesInfo['routesById'],
TStrict extends boolean = true,
@@ -476,11 +459,11 @@ export function useParams<
}): TSelected {
const router = useRouterContext()
useStore(router.__store, (d) => {
- const params = last(d.currentMatches)?.params as any
+ const params = last(d.matches)?.params as any
return opts?.track?.(params) ?? params
})
- return last(router.state.currentMatches)?.params as any
+ return last(router.state.matches)?.params as any
}
export function useNavigate<
@@ -569,8 +552,28 @@ function SubOutlet({
match.route.options.wrapInSuspense ?? !match.route.isRoot
? React.Suspense
: SafeFragment
+
const ResolvedCatchBoundary = errorComponent ? CatchBoundary : SafeFragment
+ const dehydrated: Record = {}
+
+ if (typeof document === 'undefined') {
+ if (match.state.loader) {
+ Object.keys(match.state.loader).forEach((key) => {
+ let value = match.state.loader[key]
+
+ if (value instanceof Promise || value.then) {
+ value = {
+ __isPromise: true,
+ key: key,
+ }
+ }
+
+ dehydrated[key] = value
+ })
+ }
+ }
+
return (
}>
@@ -581,6 +584,20 @@ function SubOutlet({
warning(false, `Error in route match: ${match.id}`)
}}
>
+ {/* {!match.route.isRoot ? (
+
+ ) : null} */}
@@ -591,6 +608,12 @@ function SubOutlet({
function Inner(props: { match: RouteMatch }): any {
const router = useRouterContext()
+ // if (!props.match.route.isRoot && typeof document !== 'undefined') {
+ // router.options.hydrate?.(
+ // (window as any).__TSR_DEHYDRATED_MATCHES__[props.match.id],
+ // )
+ // }
+
if (props.match.state.status === 'error') {
throw props.match.state.error
}
@@ -669,13 +692,13 @@ function CatchBoundaryInner(props: {
React.useEffect(() => {
if (activeErrorState) {
- if (router.state.currentLocation.key !== prevKeyRef.current) {
+ if (router.state.location.key !== prevKeyRef.current) {
// setActiveErrorState({} as any)
}
}
- prevKeyRef.current = router.state.currentLocation.key
- }, [activeErrorState, router.state.currentLocation.key])
+ prevKeyRef.current = router.state.location.key
+ }, [activeErrorState, router.state.location.key])
React.useEffect(() => {
if (props.errorState.error) {
diff --git a/packages/router/src/route.ts b/packages/router/src/route.ts
index 46f7f08654..486a145489 100644
--- a/packages/router/src/route.ts
+++ b/packages/router/src/route.ts
@@ -191,6 +191,16 @@ export type RouteOptions<
> = MergeFromParent,
> = RouteOptionsBase & {
getParentRoute: () => TParentRoute
+ // Optionally call this function to get a unique key for this route.
+ // This is useful for routes that need to be uniquely identified
+ // by more than their by search params
+ getKey?: OnLoadFnKey<
+ TSearchSchema,
+ TFullSearchSchema,
+ TAllParams,
+ NoInfer,
+ TContext
+ >
// If true, this route will be matched as case-sensitive
caseSensitive?: boolean
// Filter functions that can manipulate search params *before* they are passed to links and navigate
@@ -362,6 +372,26 @@ export type OnLoadFn<
>,
) => Promise | TLoader
+export type OnLoadFnKey<
+ TSearchSchema extends AnySearchSchema = {},
+ TFullSearchSchema extends AnySearchSchema = {},
+ TAllParams = {},
+ TContext extends AnyContext = AnyContext,
+ TAllContext extends AnyContext = AnyContext,
+> = (
+ loaderContext: {
+ params: TAllParams
+ search: TFullSearchSchema
+ },
+ // loaderContext: LoaderContext<
+ // TSearchSchema,
+ // TFullSearchSchema,
+ // TAllParams,
+ // TContext,
+ // TAllContext
+ // >,
+) => any
+
export interface LoaderContext<
TSearchSchema extends AnySearchSchema = {},
TFullSearchSchema extends AnySearchSchema = {},
@@ -376,6 +406,14 @@ export interface LoaderContext<
preload: boolean
routeContext: TContext
context: TAllContext
+ // serverOnly: <
+ // TServer extends object | (() => object),
+ // TClient extends object | (() => object),
+ // >(
+ // server: TServer,
+ // client: TClient,
+ // ) => (TServer extends () => infer TReturn ? TReturn : TServer) &
+ // (TClient extends () => infer TReturn ? TReturn : TClient)
}
export type UnloaderFn = (
diff --git a/packages/router/src/routeMatch.ts b/packages/router/src/routeMatch.ts
index 30103196b8..34e403cd9f 100644
--- a/packages/router/src/routeMatch.ts
+++ b/packages/router/src/routeMatch.ts
@@ -13,7 +13,7 @@ export interface RouteMatchState<
routeSearch: TRoute['__types']['searchSchema']
search: TRoutesInfo['fullSearchSchema'] &
TRoute['__types']['fullSearchSchema']
- status: 'idle' | 'pending' | 'success' | 'error'
+ status: 'pending' | 'success' | 'error'
error?: unknown
updatedAt: number
loader: TRoute['__types']['loader']
@@ -64,7 +64,9 @@ export class RouteMatch<
parentMatch?: RouteMatch
pendingInfo?: PendingRouteMatchInfo
+ __loadKey: any = { __init: true }
__loadPromise?: Promise
+ __loadPromiseResolve?: () => void
__onExit?:
| void
| ((matchContext: {
@@ -92,7 +94,7 @@ export class RouteMatch<
updatedAt: 0,
routeSearch: {},
search: {} as any,
- status: 'idle',
+ status: 'pending',
loader: undefined,
},
{
@@ -111,11 +113,16 @@ export class RouteMatch<
this[type] = component as any
})
- if (this.state.status === 'idle' && !this.#hasLoaders()) {
+ this.__loadPromise = new Promise((r) => {
+ this.__loadPromiseResolve = r
+ })
+
+ if (this.state.status === 'pending' && !this.#hasLoaders()) {
this.__store.setState((s) => ({
...s,
status: 'success',
}))
+ this.__loadPromiseResolve?.()
}
}
@@ -128,7 +135,7 @@ export class RouteMatch<
__commit = () => {
const { routeSearch, search, context, routeContext } = this.#resolveInfo({
- location: this.router.state.currentLocation,
+ location: this.router.state.location,
})
this.context = context
this.routeContext = routeContext
@@ -246,11 +253,28 @@ export class RouteMatch<
const { routeSearch, search, context, routeContext } = info
- // If the match is invalid, errored or idle, trigger it to load
- if (this.state.status === 'pending') {
- return
+ const loaderOpts = {
+ params: this.params,
+ routeSearch,
+ search,
+ signal: this.abortController.signal,
+ preload: !!opts?.preload,
+ routeContext,
+ context,
}
+ // If getKey is set, we can skip the loader if the key is the same
+ // if (this.route.options.getKey) {
+ // const prevKey = this.__loadKey
+ // this.__loadKey = this.route.options.getKey?.(loaderOpts)
+
+ // if (
+ // !opts.preload &&
+ // JSON.stringify(prevKey) === JSON.stringify(this.__loadKey)
+ // ) {
+ // return
+ // }
+
this.__loadPromise = Promise.resolve().then(async () => {
const loadId = '' + Date.now() + Math.random()
this.#latestId = loadId
@@ -261,16 +285,6 @@ export class RouteMatch<
let latestPromise
- // If the match was in an error state, set it
- // to a loading state again. Otherwise, keep it
- // as loading or resolved
- if (this.state.status === 'idle') {
- this.__store.setState((s) => ({
- ...s,
- status: 'pending',
- }))
- }
-
const componentsPromise = (async () => {
// then run all component and data loaders in parallel
// For each component type, potentially load it asynchronously
@@ -288,15 +302,7 @@ export class RouteMatch<
const loaderPromise = Promise.resolve().then(() => {
if (this.route.options.loader) {
- return this.route.options.loader({
- params: this.params,
- routeSearch,
- search,
- signal: this.abortController.signal,
- preload: !!opts?.preload,
- routeContext: routeContext,
- context: context,
- })
+ return this.route.options.loader(loaderOpts)
}
return
})
@@ -350,6 +356,7 @@ export class RouteMatch<
updatedAt: Date.now(),
}))
} finally {
+ this.__loadPromiseResolve?.()
delete this.__loadPromise
}
})
diff --git a/packages/router/src/router.ts b/packages/router/src/router.ts
index d583de1226..5019819942 100644
--- a/packages/router/src/router.ts
+++ b/packages/router/src/router.ts
@@ -154,11 +154,9 @@ export interface RouterState<
TState extends LocationState = LocationState,
> {
status: 'idle' | 'pending'
- latestLocation: ParsedLocation
- currentMatches: RouteMatch[]
+ matches: RouteMatch[]
+ location: ParsedLocation
currentLocation: ParsedLocation
- pendingMatches?: RouteMatch[]
- pendingLocation?: ParsedLocation
lastUpdated: number
}
@@ -201,12 +199,7 @@ type LinkCurrentTargetElement = {
}
export interface DehydratedRouterState
- extends Pick<
- RouterState,
- 'status' | 'latestLocation' | 'currentLocation' | 'lastUpdated'
- > {
- // currentMatches: DehydratedRouteMatch[]
-}
+ extends Pick {}
export interface DehydratedRouter {
state: DehydratedRouterState
@@ -311,7 +304,7 @@ export class Router<
state: true,
})
- if (this.state.latestLocation.href !== next.href) {
+ if (this.state.location.href !== next.href) {
this.#commitLocation({ ...next, replace: true })
}
}
@@ -324,7 +317,7 @@ export class Router<
// Mount only does anything on the client
if (!isServer) {
// If the router matches are empty, start loading the matches
- if (!this.state.currentMatches.length) {
+ if (!this.state.matches.length) {
this.safeLoad()
}
}
@@ -353,13 +346,13 @@ export class Router<
this.__store.setState((s) => ({
...s,
- latestLocation: parsedLocation,
currentLocation: parsedLocation,
+ location: parsedLocation,
}))
this.#unsubHistory = this.history.listen(() => {
this.safeLoad({
- next: this.#parseLocation(this.state.latestLocation),
+ next: this.#parseLocation(this.state.location),
})
})
}
@@ -379,7 +372,7 @@ export class Router<
buildNext = (opts: BuildNextOptions): ParsedLocation => {
const next = this.#buildLocation(opts)
- const __matches = this.matchRoutes(next.pathname)
+ const __matches = this.matchRoutes(next.pathname, next.search)
return this.#buildLocation({
...opts,
@@ -388,10 +381,7 @@ export class Router<
}
cancelMatches = () => {
- ;[
- ...this.state.currentMatches,
- ...(this.state.pendingMatches || []),
- ].forEach((match) => {
+ ;[...this.state.matches].forEach((match) => {
match.cancel()
})
}
@@ -404,6 +394,8 @@ export class Router<
}
load = async (opts?: { next?: ParsedLocation }): Promise => {
+ this.#createNavigationPromise()
+
let now = Date.now()
const startedAt = now
this.startedLoadingAt = startedAt
@@ -418,37 +410,36 @@ export class Router<
// Ingest the new location
this.__store.setState((s) => ({
...s,
- latestLocation: opts.next!,
+ location: opts.next!,
}))
}
// Match the routes
- matches = this.matchRoutes(this.state.latestLocation.pathname, {
- strictParseParams: true,
- debug: true,
- })
+ matches = this.matchRoutes(
+ this.state.location.pathname,
+ this.state.location.search,
+ {
+ strictParseParams: true,
+ debug: true,
+ },
+ )
this.__store.setState((s) => ({
...s,
status: 'pending',
- pendingMatches: matches,
- pendingLocation: this.state.latestLocation,
+ matches: matches,
}))
})
// Load the matches
- await this.loadMatches(
- matches,
- this.state.pendingLocation!,
- // opts
- )
+ await this.loadMatches(matches, this.state.location)
if (this.startedLoadingAt !== startedAt) {
// Ignore side-effects of outdated side-effects
return this.navigationPromise
}
- const previousMatches = this.state.currentMatches
+ const previousMatches = this.state.matches
const exiting: AnyRouteMatch[] = [],
staying: AnyRouteMatch[] = []
@@ -497,22 +488,20 @@ export class Router<
})
})
- const prevLocation = this.state.currentLocation
+ const prevLocation = this.state.location
this.__store.setState((s) => ({
...s,
status: 'idle',
- currentLocation: this.state.latestLocation,
- currentMatches: matches,
- pendingLocation: undefined,
- pendingMatches: undefined,
+ currentLocation: s.location,
+ matches: matches,
}))
matches.forEach((match) => {
match.__commit()
})
- if (prevLocation!.href !== this.state.currentLocation.href) {
+ if (prevLocation!.href !== this.state.location.href) {
this.options.onRouteChange?.()
}
@@ -530,10 +519,10 @@ export class Router<
}
loadRoute = async (
- navigateOpts: BuildNextOptions = this.state.latestLocation,
+ navigateOpts: BuildNextOptions = this.state.location,
): Promise => {
const next = this.buildNext(navigateOpts)
- const matches = this.matchRoutes(next.pathname, {
+ const matches = this.matchRoutes(next.pathname, next.search, {
strictParseParams: true,
})
await this.loadMatches(matches, next)
@@ -541,10 +530,10 @@ export class Router<
}
preloadRoute = async (
- navigateOpts: BuildNextOptions = this.state.latestLocation,
+ navigateOpts: BuildNextOptions = this.state.location,
) => {
const next = this.buildNext(navigateOpts)
- const matches = this.matchRoutes(next.pathname, {
+ const matches = this.matchRoutes(next.pathname, next.search, {
strictParseParams: true,
})
@@ -556,6 +545,7 @@ export class Router<
matchRoutes = (
pathname: string,
+ search: AnySearchSchema,
opts?: { strictParseParams?: boolean; debug?: boolean },
): RouteMatch[] => {
// If there's no route tree, we can't match anything
@@ -565,10 +555,7 @@ export class Router<
// Existing matches are matches that are already loaded along with
// pending matches that are still loading
- const existingMatches = [
- ...this.state.currentMatches,
- ...(this.state.pendingMatches ?? []),
- ]
+ const existingMatches = [...this.state.matches]
// We need to "flatten" layout routes, but only process as many
// routes as we need to in order to find the best match
@@ -682,7 +669,12 @@ export class Router<
Object.assign(allParams, params)
const interpolatedPath = interpolatePath(route.path, allParams)
- const matchId = interpolatePath(route.id, allParams, true)
+ const matchId =
+ interpolatePath(route.id, allParams, true) +
+ route.options.getKey?.({
+ params: allParams,
+ search,
+ })
// Waste not, want not. If we already have a match for this route,
// reuse it. This is important for layout routes, which might stick
@@ -851,8 +843,12 @@ export class Router<
} as any
const next = this.buildNext(location)
+ if (opts?.pending && this.state.status !== 'pending') {
+ return false
+ }
+
const baseLocation = opts?.pending
- ? this.state.pendingLocation
+ ? this.state.location
: this.state.currentLocation
if (!baseLocation) {
@@ -918,21 +914,21 @@ export class Router<
userPreloadDelay ?? this.options.defaultPreloadDelay ?? 0
// Compare path/hash for matches
- const currentPathSplit = this.state.currentLocation.pathname.split('/')
+ const currentPathSplit = this.state.location.pathname.split('/')
const nextPathSplit = next.pathname.split('/')
const pathIsFuzzyEqual = nextPathSplit.every(
(d, i) => d === currentPathSplit[i],
)
// Combine the matches based on user options
const pathTest = activeOptions?.exact
- ? this.state.currentLocation.pathname === next.pathname
+ ? this.state.location.pathname === next.pathname
: pathIsFuzzyEqual
const hashTest = activeOptions?.includeHash
- ? this.state.currentLocation.hash === next.hash
+ ? this.state.location.hash === next.hash
: true
const searchTest =
activeOptions?.includeSearch ?? true
- ? partialDeepEqual(this.state.currentLocation.search, next.search)
+ ? partialDeepEqual(this.state.location.search, next.search)
: true
// The final "active" test
@@ -1014,12 +1010,7 @@ export class Router<
dehydrate = (): DehydratedRouter => {
return {
state: {
- ...pick(this.state, [
- 'latestLocation',
- 'currentLocation',
- 'status',
- 'lastUpdated',
- ]),
+ ...pick(this.state, ['location', 'status', 'lastUpdated']),
},
}
}
@@ -1044,6 +1035,7 @@ export class Router<
return {
...s,
...ctx.router.state,
+ currentLocation: ctx.router.state.location,
}
})
@@ -1177,8 +1169,8 @@ export class Router<
dest.fromCurrent = dest.fromCurrent ?? dest.to === ''
const fromPathname = dest.fromCurrent
- ? this.state.latestLocation.pathname
- : dest.from ?? this.state.latestLocation.pathname
+ ? this.state.location.pathname
+ : dest.from ?? this.state.location.pathname
let pathname = resolvePath(
this.basepath ?? '/',
@@ -1186,9 +1178,13 @@ export class Router<
`${dest.to ?? ''}`,
)
- const fromMatches = this.matchRoutes(this.state.latestLocation.pathname, {
- strictParseParams: true,
- })
+ const fromMatches = this.matchRoutes(
+ this.state.location.pathname,
+ this.state.location.search,
+ {
+ strictParseParams: true,
+ },
+ )
const prevParams = { ...last(fromMatches)?.params }
@@ -1224,9 +1220,9 @@ export class Router<
const preFilteredSearch = preSearchFilters?.length
? preSearchFilters?.reduce(
(prev, next) => next(prev),
- this.state.latestLocation.search,
+ this.state.location.search,
)
- : this.state.latestLocation.search
+ : this.state.location.search
// Then the link/navigate function
const destSearch =
@@ -1244,7 +1240,7 @@ export class Router<
: destSearch
const search = replaceEqualDeep(
- this.state.latestLocation.search,
+ this.state.location.search,
postFilteredSearch,
)
@@ -1252,15 +1248,15 @@ export class Router<
const hash =
dest.hash === true
- ? this.state.latestLocation.hash
- : functionalUpdate(dest.hash!, this.state.latestLocation.hash)
+ ? this.state.location.hash
+ : functionalUpdate(dest.hash!, this.state.location.hash)
const hashStr = hash ? `#${hash}` : ''
const nextState =
dest.state === true
- ? this.state.latestLocation.state
- : functionalUpdate(dest.state, this.state.latestLocation.state)!
+ ? this.state.location.state
+ : functionalUpdate(dest.state, this.state.location.state)!
return {
pathname,
@@ -1287,7 +1283,7 @@ export class Router<
nextAction = 'push'
}
- const isSameUrl = this.state.latestLocation.href === next.href
+ const isSameUrl = this.state.location.href === next.href
if (isSameUrl && !next.key) {
nextAction = 'replace'
@@ -1302,14 +1298,20 @@ export class Router<
...next.state,
})
- return (this.navigationPromise = new Promise((resolve) => {
- const previousNavigationResolve = this.resolveNavigation
+ return this.#createNavigationPromise()
+ }
+
+ #createNavigationPromise = () => {
+ const previousNavigationResolve = this.resolveNavigation
+ this.navigationPromise = new Promise((resolve) => {
this.resolveNavigation = () => {
- previousNavigationResolve()
resolve()
+ previousNavigationResolve()
}
- }))
+ })
+
+ return this.navigationPromise
}
}
@@ -1319,9 +1321,9 @@ const isServer = typeof window === 'undefined' || !window.document.createElement
function getInitialRouterState(): RouterState {
return {
status: 'idle',
- latestLocation: null!,
currentLocation: null!,
- currentMatches: [],
+ location: null!,
+ matches: [],
lastUpdated: Date.now(),
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6221c8a90e..a624bc1abb 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -298,6 +298,9 @@ importers:
'@tanstack/react-loaders':
specifier: workspace:*
version: link:../../../packages/react-loaders
+ '@tanstack/react-start':
+ specifier: workspace:*
+ version: link:../../../packages/react-start
'@tanstack/router':
specifier: workspace:*
version: link:../../../packages/router