diff --git a/packages/react-router/src/useRouterState.tsx b/packages/react-router/src/useRouterState.tsx index 08a7c7b9b8..4f9ef36907 100644 --- a/packages/react-router/src/useRouterState.tsx +++ b/packages/react-router/src/useRouterState.tsx @@ -1,6 +1,7 @@ import { useStore } from '@tanstack/react-store' import { useRef } from 'react' import { replaceEqualDeep } from '@tanstack/router-core' +import { isServer } from '@tanstack/router-core/isServer' import { useRouter } from './useRouter' import type { AnyRouter, @@ -51,9 +52,23 @@ export function useRouterState< warn: opts?.router === undefined, }) const router = opts?.router || contextRouter + + // During SSR we render exactly once and do not need reactivity. + // Avoid subscribing to the store (and any structural sharing work) on the server. + const _isServer = isServer ?? router.isServer + if (_isServer) { + const state = router.state as RouterState + return (opts?.select ? opts.select(state) : state) as UseRouterStateResult< + TRouter, + TSelected + > + } + const previousResult = + // eslint-disable-next-line react-hooks/rules-of-hooks useRef>(undefined) + // eslint-disable-next-line react-hooks/rules-of-hooks return useStore(router.__store, (state) => { if (opts?.select) { if (opts.structuralSharing ?? router.options.defaultStructuralSharing) { diff --git a/packages/router-core/src/load-matches.ts b/packages/router-core/src/load-matches.ts index 1cca25ed05..957155fef3 100644 --- a/packages/router-core/src/load-matches.ts +++ b/packages/router-core/src/load-matches.ts @@ -1,6 +1,6 @@ -import { batch } from '@tanstack/store' import invariant from 'tiny-invariant' import { isServer } from '@tanstack/router-core/isServer' +import { batch } from './utils/batch' import { createControlledPromise, isPromise } from './utils' import { isNotFound } from './not-found' import { rootRouteId } from './root' diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts index 6ffbd0b493..dd1267e8d5 100644 --- a/packages/router-core/src/router.ts +++ b/packages/router-core/src/router.ts @@ -1,6 +1,7 @@ -import { Store, batch } from '@tanstack/store' +import { Store } from '@tanstack/store' import { createBrowserHistory, parseHref } from '@tanstack/history' import { isServer } from '@tanstack/router-core/isServer' +import { batch } from './utils/batch' import { createControlledPromise, decodePath, @@ -898,6 +899,29 @@ declare global { * * @link https://tanstack.com/router/latest/docs/framework/react/api/router/RouterType */ +type RouterStateStore = { + state: TState + setState: (updater: (prev: TState) => TState) => void +} + +function createServerStore( + initialState: TState, +): RouterStateStore { + let state = initialState + + return { + get state() { + return state + }, + set state(next) { + state = next + }, + setState: (updater: (prev: TState) => TState) => { + state = updater(state) + }, + } as RouterStateStore +} + export class RouterCore< in out TRouteTree extends AnyRoute, in out TTrailingSlashOption extends TrailingSlashOption, @@ -972,8 +996,8 @@ export class RouterCore< } } - // These are default implementations that can optionally be overridden - // by the router provider once rendered. We provide these so that the + // This is a default implementation that can optionally be overridden + // by the router provider once rendered. We provide this so that the // router can be used in a non-react environment if necessary startTransition: StartTransitionFn = (fn) => fn() @@ -1076,18 +1100,24 @@ export class RouterCore< } if (!this.__store && this.latestLocation) { - this.__store = new Store(getInitialRouterState(this.latestLocation), { - onUpdate: () => { - this.__store.state = { - ...this.state, - cachedMatches: this.state.cachedMatches.filter( - (d) => !['redirected'].includes(d.status), - ), - } - }, - }) + if (isServer ?? this.isServer) { + this.__store = createServerStore( + getInitialRouterState(this.latestLocation), + ) as unknown as Store + } else { + this.__store = new Store(getInitialRouterState(this.latestLocation), { + onUpdate: () => { + this.__store.state = { + ...this.state, + cachedMatches: this.state.cachedMatches.filter( + (d) => !['redirected'].includes(d.status), + ), + } + }, + }) - setupScrollRestoration(this) + setupScrollRestoration(this) + } } let needsLocationUpdate = false diff --git a/packages/router-core/src/ssr/ssr-client.ts b/packages/router-core/src/ssr/ssr-client.ts index 67d83d2d61..2afbcbd17b 100644 --- a/packages/router-core/src/ssr/ssr-client.ts +++ b/packages/router-core/src/ssr/ssr-client.ts @@ -1,5 +1,5 @@ import invariant from 'tiny-invariant' -import { batch } from '@tanstack/store' +import { batch } from '../utils/batch' import { isNotFound } from '../not-found' import { createControlledPromise } from '../utils' import type { GLOBAL_SEROVAL, GLOBAL_TSR } from './constants' diff --git a/packages/router-core/src/utils.ts b/packages/router-core/src/utils.ts index cafe47047a..f8fc8d5659 100644 --- a/packages/router-core/src/utils.ts +++ b/packages/router-core/src/utils.ts @@ -1,3 +1,4 @@ +import { isServer } from '@tanstack/router-core/isServer' import type { RouteIds } from './routeInfo' import type { AnyRouter } from './router' @@ -221,6 +222,9 @@ const isEnumerable = Object.prototype.propertyIsEnumerable * Do not use this with signals */ export function replaceEqualDeep(prev: any, _next: T, _depth = 0): T { + if (isServer) { + return _next + } if (prev === _next) { return prev } diff --git a/packages/router-core/src/utils/batch.ts b/packages/router-core/src/utils/batch.ts new file mode 100644 index 0000000000..e454719fff --- /dev/null +++ b/packages/router-core/src/utils/batch.ts @@ -0,0 +1,18 @@ +import { batch as storeBatch } from '@tanstack/store' + +import { isServer } from '@tanstack/router-core/isServer' + +// `@tanstack/store`'s `batch` is for reactive notification batching. +// On the server we don't subscribe/render reactively, so a lightweight +// implementation that just executes is enough. +export function batch(fn: () => T): T { + if (isServer) { + return fn() + } + + let result!: T + storeBatch(() => { + result = fn() + }) + return result +} diff --git a/packages/solid-router/src/useRouterState.tsx b/packages/solid-router/src/useRouterState.tsx index 145455d0df..b9ed55dc5e 100644 --- a/packages/solid-router/src/useRouterState.tsx +++ b/packages/solid-router/src/useRouterState.tsx @@ -1,4 +1,5 @@ import { useStore } from '@tanstack/solid-store' +import { isServer } from '@tanstack/router-core/isServer' import { useRouter } from './useRouter' import type { AnyRouter, @@ -54,6 +55,20 @@ export function useRouterState< }) const router = opts?.router || contextRouter + // During SSR we render exactly once and do not need reactivity. + // Avoid subscribing to the store on the server since the server store + // implementation does not provide subscribe() semantics. + const _isServer = isServer ?? router.isServer + if (_isServer) { + const state = router.state as RouterState + const selected = ( + opts?.select ? opts.select(state) : state + ) as UseRouterStateResult + return (() => selected) as Accessor< + UseRouterStateResult + > + } + return useStore( router.__store, (state) => { diff --git a/packages/vue-router/src/useRouterState.tsx b/packages/vue-router/src/useRouterState.tsx index 7937d3b6d3..9f621a6a2a 100644 --- a/packages/vue-router/src/useRouterState.tsx +++ b/packages/vue-router/src/useRouterState.tsx @@ -1,5 +1,6 @@ import { useStore } from '@tanstack/vue-store' import * as Vue from 'vue' +import { isServer } from '@tanstack/router-core/isServer' import { useRouter } from './useRouter' import type { AnyRouter, @@ -35,6 +36,18 @@ export function useRouterState< > } + // During SSR we render exactly once and do not need reactivity. + // Avoid subscribing to the store on the server since the server store + // implementation does not provide subscribe() semantics. + const _isServer = isServer ?? router.isServer + + if (_isServer) { + const state = router.state as RouterState + return Vue.ref(opts?.select ? opts.select(state) : state) as Vue.Ref< + UseRouterStateResult + > + } + return useStore(router.__store, (state) => { if (opts?.select) return opts.select(state)