@tanstack/react-router 1.167.5 → 1.168.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/Match.cjs +118 -52
- package/dist/cjs/Match.cjs.map +1 -1
- package/dist/cjs/Matches.cjs +20 -20
- package/dist/cjs/Matches.cjs.map +1 -1
- package/dist/cjs/Scripts.cjs +36 -32
- package/dist/cjs/Scripts.cjs.map +1 -1
- package/dist/cjs/Transitioner.cjs +10 -16
- package/dist/cjs/Transitioner.cjs.map +1 -1
- package/dist/cjs/headContentUtils.cjs +147 -59
- package/dist/cjs/headContentUtils.cjs.map +1 -1
- package/dist/cjs/index.cjs +1 -1
- package/dist/cjs/index.dev.cjs +1 -1
- package/dist/cjs/link.cjs +34 -29
- package/dist/cjs/link.cjs.map +1 -1
- package/dist/cjs/not-found.cjs +20 -2
- package/dist/cjs/not-found.cjs.map +1 -1
- package/dist/cjs/router.cjs +2 -1
- package/dist/cjs/router.cjs.map +1 -1
- package/dist/cjs/routerStores.cjs +21 -0
- package/dist/cjs/routerStores.cjs.map +1 -0
- package/dist/cjs/routerStores.d.cts +7 -0
- package/dist/cjs/ssr/RouterClient.cjs +1 -1
- package/dist/cjs/ssr/RouterClient.cjs.map +1 -1
- package/dist/cjs/ssr/renderRouterToStream.cjs +2 -2
- package/dist/cjs/ssr/renderRouterToStream.cjs.map +1 -1
- package/dist/cjs/ssr/renderRouterToString.cjs +1 -1
- package/dist/cjs/ssr/renderRouterToString.cjs.map +1 -1
- package/dist/cjs/useCanGoBack.cjs +7 -2
- package/dist/cjs/useCanGoBack.cjs.map +1 -1
- package/dist/cjs/useLocation.cjs +21 -2
- package/dist/cjs/useLocation.cjs.map +1 -1
- package/dist/cjs/useMatch.cjs +29 -9
- package/dist/cjs/useMatch.cjs.map +1 -1
- package/dist/cjs/useRouterState.cjs +2 -2
- package/dist/cjs/useRouterState.cjs.map +1 -1
- package/dist/esm/Match.js +118 -52
- package/dist/esm/Match.js.map +1 -1
- package/dist/esm/Matches.js +21 -21
- package/dist/esm/Matches.js.map +1 -1
- package/dist/esm/Scripts.js +36 -32
- package/dist/esm/Scripts.js.map +1 -1
- package/dist/esm/Transitioner.js +10 -16
- package/dist/esm/Transitioner.js.map +1 -1
- package/dist/esm/headContentUtils.js +148 -60
- package/dist/esm/headContentUtils.js.map +1 -1
- package/dist/esm/index.dev.js +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/link.js +34 -29
- package/dist/esm/link.js.map +1 -1
- package/dist/esm/not-found.js +20 -2
- package/dist/esm/not-found.js.map +1 -1
- package/dist/esm/router.js +2 -1
- package/dist/esm/router.js.map +1 -1
- package/dist/esm/routerStores.d.ts +7 -0
- package/dist/esm/routerStores.js +20 -0
- package/dist/esm/routerStores.js.map +1 -0
- package/dist/esm/ssr/RouterClient.js +1 -1
- package/dist/esm/ssr/RouterClient.js.map +1 -1
- package/dist/esm/ssr/renderRouterToStream.js +2 -2
- package/dist/esm/ssr/renderRouterToStream.js.map +1 -1
- package/dist/esm/ssr/renderRouterToString.js +1 -1
- package/dist/esm/ssr/renderRouterToString.js.map +1 -1
- package/dist/esm/useCanGoBack.js +6 -2
- package/dist/esm/useCanGoBack.js.map +1 -1
- package/dist/esm/useLocation.js +20 -2
- package/dist/esm/useLocation.js.map +1 -1
- package/dist/esm/useMatch.js +29 -9
- package/dist/esm/useMatch.js.map +1 -1
- package/dist/esm/useRouterState.js +2 -2
- package/dist/esm/useRouterState.js.map +1 -1
- package/dist/llms/rules/api.d.ts +1 -1
- package/dist/llms/rules/api.js +3 -9
- package/package.json +2 -2
- package/src/Match.tsx +218 -78
- package/src/Matches.tsx +45 -25
- package/src/Scripts.tsx +72 -44
- package/src/Transitioner.tsx +24 -16
- package/src/headContentUtils.tsx +210 -27
- package/src/link.tsx +66 -71
- package/src/not-found.tsx +41 -4
- package/src/router.ts +2 -1
- package/src/routerStores.ts +26 -0
- package/src/ssr/RouterClient.tsx +1 -1
- package/src/ssr/renderRouterToStream.tsx +2 -2
- package/src/ssr/renderRouterToString.tsx +1 -1
- package/src/useCanGoBack.ts +14 -2
- package/src/useLocation.tsx +32 -5
- package/src/useMatch.tsx +61 -21
- package/src/useRouterState.tsx +4 -2
package/src/Transitioner.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
|
+
import { batch, useStore } from '@tanstack/react-store'
|
|
2
3
|
import {
|
|
3
4
|
getLocationChangeInfo,
|
|
4
5
|
handleHashScroll,
|
|
@@ -6,7 +7,6 @@ import {
|
|
|
6
7
|
} from '@tanstack/router-core'
|
|
7
8
|
import { useLayoutEffect, usePrevious } from './utils'
|
|
8
9
|
import { useRouter } from './useRouter'
|
|
9
|
-
import { useRouterState } from './useRouterState'
|
|
10
10
|
|
|
11
11
|
export function Transitioner() {
|
|
12
12
|
const router = useRouter()
|
|
@@ -14,13 +14,11 @@ export function Transitioner() {
|
|
|
14
14
|
|
|
15
15
|
const [isTransitioning, setIsTransitioning] = React.useState(false)
|
|
16
16
|
// Track pending state changes
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
structuralSharing: true,
|
|
23
|
-
})
|
|
17
|
+
const isLoading = useStore(router.stores.isLoading, (value) => value)
|
|
18
|
+
const hasPendingMatches = useStore(
|
|
19
|
+
router.stores.hasPendingMatches,
|
|
20
|
+
(value) => value,
|
|
21
|
+
)
|
|
24
22
|
|
|
25
23
|
const previousIsLoading = usePrevious(isLoading)
|
|
26
24
|
|
|
@@ -95,7 +93,10 @@ export function Transitioner() {
|
|
|
95
93
|
if (previousIsLoading && !isLoading) {
|
|
96
94
|
router.emit({
|
|
97
95
|
type: 'onLoad', // When the new URL has committed, when the new matches have been loaded into state.matches
|
|
98
|
-
...getLocationChangeInfo(
|
|
96
|
+
...getLocationChangeInfo(
|
|
97
|
+
router.stores.location.state,
|
|
98
|
+
router.stores.resolvedLocation.state,
|
|
99
|
+
),
|
|
99
100
|
})
|
|
100
101
|
}
|
|
101
102
|
}, [previousIsLoading, router, isLoading])
|
|
@@ -105,24 +106,31 @@ export function Transitioner() {
|
|
|
105
106
|
if (previousIsPagePending && !isPagePending) {
|
|
106
107
|
router.emit({
|
|
107
108
|
type: 'onBeforeRouteMount',
|
|
108
|
-
...getLocationChangeInfo(
|
|
109
|
+
...getLocationChangeInfo(
|
|
110
|
+
router.stores.location.state,
|
|
111
|
+
router.stores.resolvedLocation.state,
|
|
112
|
+
),
|
|
109
113
|
})
|
|
110
114
|
}
|
|
111
115
|
}, [isPagePending, previousIsPagePending, router])
|
|
112
116
|
|
|
113
117
|
useLayoutEffect(() => {
|
|
114
118
|
if (previousIsAnyPending && !isAnyPending) {
|
|
115
|
-
const changeInfo = getLocationChangeInfo(
|
|
119
|
+
const changeInfo = getLocationChangeInfo(
|
|
120
|
+
router.stores.location.state,
|
|
121
|
+
router.stores.resolvedLocation.state,
|
|
122
|
+
)
|
|
116
123
|
router.emit({
|
|
117
124
|
type: 'onResolved',
|
|
118
125
|
...changeInfo,
|
|
119
126
|
})
|
|
120
127
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
128
|
+
batch(() => {
|
|
129
|
+
router.stores.status.setState(() => 'idle')
|
|
130
|
+
router.stores.resolvedLocation.setState(
|
|
131
|
+
() => router.stores.location.state,
|
|
132
|
+
)
|
|
133
|
+
})
|
|
126
134
|
|
|
127
135
|
if (changeInfo.hrefChanged) {
|
|
128
136
|
handleHashScroll(router)
|
package/src/headContentUtils.tsx
CHANGED
|
@@ -1,9 +1,171 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
|
-
import {
|
|
2
|
+
import { useStore } from '@tanstack/react-store'
|
|
3
|
+
import { deepEqual, escapeHtml } from '@tanstack/router-core'
|
|
4
|
+
import { isServer } from '@tanstack/router-core/isServer'
|
|
3
5
|
import { useRouter } from './useRouter'
|
|
4
|
-
import { useRouterState } from './useRouterState'
|
|
5
6
|
import type { RouterManagedTag } from '@tanstack/router-core'
|
|
6
7
|
|
|
8
|
+
function buildTagsFromMatches(
|
|
9
|
+
router: ReturnType<typeof useRouter>,
|
|
10
|
+
nonce: string | undefined,
|
|
11
|
+
matches: Array<any>,
|
|
12
|
+
): Array<RouterManagedTag> {
|
|
13
|
+
const routeMeta = matches.map((match) => match.meta!).filter(Boolean)
|
|
14
|
+
|
|
15
|
+
const resultMeta: Array<RouterManagedTag> = []
|
|
16
|
+
const metaByAttribute: Record<string, true> = {}
|
|
17
|
+
let title: RouterManagedTag | undefined
|
|
18
|
+
for (let i = routeMeta.length - 1; i >= 0; i--) {
|
|
19
|
+
const metas = routeMeta[i]!
|
|
20
|
+
for (let j = metas.length - 1; j >= 0; j--) {
|
|
21
|
+
const m = metas[j]
|
|
22
|
+
if (!m) continue
|
|
23
|
+
|
|
24
|
+
if (m.title) {
|
|
25
|
+
if (!title) {
|
|
26
|
+
title = {
|
|
27
|
+
tag: 'title',
|
|
28
|
+
children: m.title,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} else if ('script:ld+json' in m) {
|
|
32
|
+
try {
|
|
33
|
+
const json = JSON.stringify(m['script:ld+json'])
|
|
34
|
+
resultMeta.push({
|
|
35
|
+
tag: 'script',
|
|
36
|
+
attrs: {
|
|
37
|
+
type: 'application/ld+json',
|
|
38
|
+
},
|
|
39
|
+
children: escapeHtml(json),
|
|
40
|
+
})
|
|
41
|
+
} catch {
|
|
42
|
+
// Skip invalid JSON-LD objects
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
const attribute = m.name ?? m.property
|
|
46
|
+
if (attribute) {
|
|
47
|
+
if (metaByAttribute[attribute]) {
|
|
48
|
+
continue
|
|
49
|
+
} else {
|
|
50
|
+
metaByAttribute[attribute] = true
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
resultMeta.push({
|
|
55
|
+
tag: 'meta',
|
|
56
|
+
attrs: {
|
|
57
|
+
...m,
|
|
58
|
+
nonce,
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (title) {
|
|
66
|
+
resultMeta.push(title)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (nonce) {
|
|
70
|
+
resultMeta.push({
|
|
71
|
+
tag: 'meta',
|
|
72
|
+
attrs: {
|
|
73
|
+
property: 'csp-nonce',
|
|
74
|
+
content: nonce,
|
|
75
|
+
},
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
resultMeta.reverse()
|
|
79
|
+
|
|
80
|
+
const constructedLinks = matches
|
|
81
|
+
.map((match) => match.links!)
|
|
82
|
+
.filter(Boolean)
|
|
83
|
+
.flat(1)
|
|
84
|
+
.map((link) => ({
|
|
85
|
+
tag: 'link',
|
|
86
|
+
attrs: {
|
|
87
|
+
...link,
|
|
88
|
+
nonce,
|
|
89
|
+
},
|
|
90
|
+
})) satisfies Array<RouterManagedTag>
|
|
91
|
+
|
|
92
|
+
const manifest = router.ssr?.manifest
|
|
93
|
+
const assetLinks = matches
|
|
94
|
+
.map((match) => manifest?.routes[match.routeId]?.assets ?? [])
|
|
95
|
+
.filter(Boolean)
|
|
96
|
+
.flat(1)
|
|
97
|
+
.filter((asset) => asset.tag === 'link')
|
|
98
|
+
.map(
|
|
99
|
+
(asset) =>
|
|
100
|
+
({
|
|
101
|
+
tag: 'link',
|
|
102
|
+
attrs: {
|
|
103
|
+
...asset.attrs,
|
|
104
|
+
suppressHydrationWarning: true,
|
|
105
|
+
nonce,
|
|
106
|
+
},
|
|
107
|
+
}) satisfies RouterManagedTag,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
const preloadLinks: Array<RouterManagedTag> = []
|
|
111
|
+
matches
|
|
112
|
+
.map((match) => router.looseRoutesById[match.routeId]!)
|
|
113
|
+
.forEach((route) =>
|
|
114
|
+
router.ssr?.manifest?.routes[route.id]?.preloads
|
|
115
|
+
?.filter(Boolean)
|
|
116
|
+
.forEach((preload) => {
|
|
117
|
+
preloadLinks.push({
|
|
118
|
+
tag: 'link',
|
|
119
|
+
attrs: {
|
|
120
|
+
rel: 'modulepreload',
|
|
121
|
+
href: preload,
|
|
122
|
+
nonce,
|
|
123
|
+
},
|
|
124
|
+
})
|
|
125
|
+
}),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
const styles = (
|
|
129
|
+
matches
|
|
130
|
+
.map((match) => match.styles!)
|
|
131
|
+
.flat(1)
|
|
132
|
+
.filter(Boolean) as Array<RouterManagedTag>
|
|
133
|
+
).map(({ children, ...attrs }) => ({
|
|
134
|
+
tag: 'style',
|
|
135
|
+
attrs: {
|
|
136
|
+
...attrs,
|
|
137
|
+
nonce,
|
|
138
|
+
},
|
|
139
|
+
children,
|
|
140
|
+
}))
|
|
141
|
+
|
|
142
|
+
const headScripts = (
|
|
143
|
+
matches
|
|
144
|
+
.map((match) => match.headScripts!)
|
|
145
|
+
.flat(1)
|
|
146
|
+
.filter(Boolean) as Array<RouterManagedTag>
|
|
147
|
+
).map(({ children, ...script }) => ({
|
|
148
|
+
tag: 'script',
|
|
149
|
+
attrs: {
|
|
150
|
+
...script,
|
|
151
|
+
nonce,
|
|
152
|
+
},
|
|
153
|
+
children,
|
|
154
|
+
}))
|
|
155
|
+
|
|
156
|
+
return uniqBy(
|
|
157
|
+
[
|
|
158
|
+
...resultMeta,
|
|
159
|
+
...preloadLinks,
|
|
160
|
+
...constructedLinks,
|
|
161
|
+
...assetLinks,
|
|
162
|
+
...styles,
|
|
163
|
+
...headScripts,
|
|
164
|
+
] as Array<RouterManagedTag>,
|
|
165
|
+
(d) => JSON.stringify(d),
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
7
169
|
/**
|
|
8
170
|
* Build the list of head/link/meta/script tags to render for active matches.
|
|
9
171
|
* Used internally by `HeadContent`.
|
|
@@ -11,12 +173,25 @@ import type { RouterManagedTag } from '@tanstack/router-core'
|
|
|
11
173
|
export const useTags = () => {
|
|
12
174
|
const router = useRouter()
|
|
13
175
|
const nonce = router.options.ssr?.nonce
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
176
|
+
|
|
177
|
+
if (isServer ?? router.isServer) {
|
|
178
|
+
return buildTagsFromMatches(
|
|
179
|
+
router,
|
|
180
|
+
nonce,
|
|
181
|
+
router.stores.activeMatchesSnapshot.state,
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static
|
|
186
|
+
const routeMeta = useStore(
|
|
187
|
+
router.stores.activeMatchesSnapshot,
|
|
188
|
+
(matches) => {
|
|
189
|
+
return matches.map((match) => match.meta!).filter(Boolean)
|
|
17
190
|
},
|
|
18
|
-
|
|
191
|
+
deepEqual,
|
|
192
|
+
)
|
|
19
193
|
|
|
194
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static
|
|
20
195
|
const meta: Array<RouterManagedTag> = React.useMemo(() => {
|
|
21
196
|
const resultMeta: Array<RouterManagedTag> = []
|
|
22
197
|
const metaByAttribute: Record<string, true> = {}
|
|
@@ -88,9 +263,11 @@ export const useTags = () => {
|
|
|
88
263
|
return resultMeta
|
|
89
264
|
}, [routeMeta, nonce])
|
|
90
265
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
266
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static
|
|
267
|
+
const links = useStore(
|
|
268
|
+
router.stores.activeMatchesSnapshot,
|
|
269
|
+
(matches) => {
|
|
270
|
+
const constructed = matches
|
|
94
271
|
.map((match) => match.links!)
|
|
95
272
|
.filter(Boolean)
|
|
96
273
|
.flat(1)
|
|
@@ -106,7 +283,7 @@ export const useTags = () => {
|
|
|
106
283
|
|
|
107
284
|
// These are the assets extracted from the ViteManifest
|
|
108
285
|
// using the `startManifestPlugin`
|
|
109
|
-
const assets =
|
|
286
|
+
const assets = matches
|
|
110
287
|
.map((match) => manifest?.routes[match.routeId]?.assets ?? [])
|
|
111
288
|
.filter(Boolean)
|
|
112
289
|
.flat(1)
|
|
@@ -125,14 +302,16 @@ export const useTags = () => {
|
|
|
125
302
|
|
|
126
303
|
return [...constructed, ...assets]
|
|
127
304
|
},
|
|
128
|
-
|
|
129
|
-
|
|
305
|
+
deepEqual,
|
|
306
|
+
)
|
|
130
307
|
|
|
131
|
-
|
|
132
|
-
|
|
308
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static
|
|
309
|
+
const preloadLinks = useStore(
|
|
310
|
+
router.stores.activeMatchesSnapshot,
|
|
311
|
+
(matches) => {
|
|
133
312
|
const preloadLinks: Array<RouterManagedTag> = []
|
|
134
313
|
|
|
135
|
-
|
|
314
|
+
matches
|
|
136
315
|
.map((match) => router.looseRoutesById[match.routeId]!)
|
|
137
316
|
.forEach((route) =>
|
|
138
317
|
router.ssr?.manifest?.routes[route.id]?.preloads
|
|
@@ -151,13 +330,15 @@ export const useTags = () => {
|
|
|
151
330
|
|
|
152
331
|
return preloadLinks
|
|
153
332
|
},
|
|
154
|
-
|
|
155
|
-
|
|
333
|
+
deepEqual,
|
|
334
|
+
)
|
|
156
335
|
|
|
157
|
-
|
|
158
|
-
|
|
336
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static
|
|
337
|
+
const styles = useStore(
|
|
338
|
+
router.stores.activeMatchesSnapshot,
|
|
339
|
+
(matches) =>
|
|
159
340
|
(
|
|
160
|
-
|
|
341
|
+
matches
|
|
161
342
|
.map((match) => match.styles!)
|
|
162
343
|
.flat(1)
|
|
163
344
|
.filter(Boolean) as Array<RouterManagedTag>
|
|
@@ -169,13 +350,15 @@ export const useTags = () => {
|
|
|
169
350
|
},
|
|
170
351
|
children,
|
|
171
352
|
})),
|
|
172
|
-
|
|
173
|
-
|
|
353
|
+
deepEqual,
|
|
354
|
+
)
|
|
174
355
|
|
|
175
|
-
|
|
176
|
-
|
|
356
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static
|
|
357
|
+
const headScripts: Array<RouterManagedTag> = useStore(
|
|
358
|
+
router.stores.activeMatchesSnapshot,
|
|
359
|
+
(matches) =>
|
|
177
360
|
(
|
|
178
|
-
|
|
361
|
+
matches
|
|
179
362
|
.map((match) => match.headScripts!)
|
|
180
363
|
.flat(1)
|
|
181
364
|
.filter(Boolean) as Array<RouterManagedTag>
|
|
@@ -187,8 +370,8 @@ export const useTags = () => {
|
|
|
187
370
|
},
|
|
188
371
|
children,
|
|
189
372
|
})),
|
|
190
|
-
|
|
191
|
-
|
|
373
|
+
deepEqual,
|
|
374
|
+
)
|
|
192
375
|
|
|
193
376
|
return uniqBy(
|
|
194
377
|
[
|
package/src/link.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
|
+
import { useStore } from '@tanstack/react-store'
|
|
2
3
|
import { flushSync } from 'react-dom'
|
|
3
4
|
import {
|
|
4
5
|
deepEqual,
|
|
@@ -9,7 +10,6 @@ import {
|
|
|
9
10
|
removeTrailingSlash,
|
|
10
11
|
} from '@tanstack/router-core'
|
|
11
12
|
import { isServer } from '@tanstack/router-core/isServer'
|
|
12
|
-
import { useRouterState } from './useRouterState'
|
|
13
13
|
import { useRouter } from './useRouter'
|
|
14
14
|
|
|
15
15
|
import { useForwardedRef, useIntersectionObserver } from './utils'
|
|
@@ -102,7 +102,7 @@ export function useLinkProps<
|
|
|
102
102
|
//
|
|
103
103
|
// For SSR parity (to avoid hydration errors), we still compute the link's
|
|
104
104
|
// active status on the server, but we avoid creating any router-state
|
|
105
|
-
// subscriptions by reading from
|
|
105
|
+
// subscriptions by reading from the location store directly.
|
|
106
106
|
//
|
|
107
107
|
// Note: `location.hash` is not available on the server.
|
|
108
108
|
// ==========================================================================
|
|
@@ -204,7 +204,7 @@ export function useLinkProps<
|
|
|
204
204
|
const isActive = (() => {
|
|
205
205
|
if (externalLink) return false
|
|
206
206
|
|
|
207
|
-
const currentLocation = router.
|
|
207
|
+
const currentLocation = router.stores.location.state
|
|
208
208
|
|
|
209
209
|
const exact = activeOptions?.exact ?? false
|
|
210
210
|
|
|
@@ -377,32 +377,13 @@ export function useLinkProps<
|
|
|
377
377
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
378
378
|
const isHydrated = useHydrated()
|
|
379
379
|
|
|
380
|
-
// subscribe to path/search/hash/params to re-build location when they change
|
|
381
|
-
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
382
|
-
const currentLocationState = useRouterState({
|
|
383
|
-
select: (s) => {
|
|
384
|
-
const leaf = s.matches[s.matches.length - 1]
|
|
385
|
-
return {
|
|
386
|
-
search: leaf?.search,
|
|
387
|
-
hash: s.location.hash,
|
|
388
|
-
path: leaf?.pathname, // path + params
|
|
389
|
-
}
|
|
390
|
-
},
|
|
391
|
-
structuralSharing: true as any,
|
|
392
|
-
})
|
|
393
|
-
|
|
394
|
-
const from = options.from
|
|
395
|
-
|
|
396
380
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
397
381
|
const _options = React.useMemo(
|
|
398
|
-
() =>
|
|
399
|
-
return { ...options, from }
|
|
400
|
-
},
|
|
382
|
+
() => options,
|
|
401
383
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
402
384
|
[
|
|
403
385
|
router,
|
|
404
|
-
|
|
405
|
-
from,
|
|
386
|
+
options.from,
|
|
406
387
|
options._fromLocation,
|
|
407
388
|
options.hash,
|
|
408
389
|
options.to,
|
|
@@ -415,11 +396,18 @@ export function useLinkProps<
|
|
|
415
396
|
)
|
|
416
397
|
|
|
417
398
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
399
|
+
const currentLocation = useStore(
|
|
400
|
+
router.stores.location,
|
|
401
|
+
(l) => l,
|
|
402
|
+
(prev, next) => prev.href === next.href,
|
|
421
403
|
)
|
|
422
404
|
|
|
405
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
406
|
+
const next = React.useMemo(() => {
|
|
407
|
+
const opts = { _fromLocation: currentLocation, ..._options }
|
|
408
|
+
return router.buildLocation(opts as any)
|
|
409
|
+
}, [router, currentLocation, _options])
|
|
410
|
+
|
|
423
411
|
// Use publicHref - it contains the correct href for display
|
|
424
412
|
// When a rewrite changes the origin, publicHref is the full URL
|
|
425
413
|
// Otherwise it's the origin-stripped path
|
|
@@ -474,54 +462,61 @@ export function useLinkProps<
|
|
|
474
462
|
}, [to, hrefOption, router.protocolAllowlist])
|
|
475
463
|
|
|
476
464
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
477
|
-
const isActive =
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
return false
|
|
488
|
-
}
|
|
489
|
-
} else {
|
|
490
|
-
const currentPathSplit = removeTrailingSlash(
|
|
491
|
-
s.location.pathname,
|
|
492
|
-
router.basepath,
|
|
493
|
-
)
|
|
494
|
-
const nextPathSplit = removeTrailingSlash(
|
|
495
|
-
next.pathname,
|
|
496
|
-
router.basepath,
|
|
497
|
-
)
|
|
498
|
-
|
|
499
|
-
const pathIsFuzzyEqual =
|
|
500
|
-
currentPathSplit.startsWith(nextPathSplit) &&
|
|
501
|
-
(currentPathSplit.length === nextPathSplit.length ||
|
|
502
|
-
currentPathSplit[nextPathSplit.length] === '/')
|
|
503
|
-
|
|
504
|
-
if (!pathIsFuzzyEqual) {
|
|
505
|
-
return false
|
|
506
|
-
}
|
|
465
|
+
const isActive = React.useMemo(() => {
|
|
466
|
+
if (externalLink) return false
|
|
467
|
+
if (activeOptions?.exact) {
|
|
468
|
+
const testExact = exactPathTest(
|
|
469
|
+
currentLocation.pathname,
|
|
470
|
+
next.pathname,
|
|
471
|
+
router.basepath,
|
|
472
|
+
)
|
|
473
|
+
if (!testExact) {
|
|
474
|
+
return false
|
|
507
475
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
476
|
+
} else {
|
|
477
|
+
const currentPathSplit = removeTrailingSlash(
|
|
478
|
+
currentLocation.pathname,
|
|
479
|
+
router.basepath,
|
|
480
|
+
)
|
|
481
|
+
const nextPathSplit = removeTrailingSlash(next.pathname, router.basepath)
|
|
482
|
+
|
|
483
|
+
const pathIsFuzzyEqual =
|
|
484
|
+
currentPathSplit.startsWith(nextPathSplit) &&
|
|
485
|
+
(currentPathSplit.length === nextPathSplit.length ||
|
|
486
|
+
currentPathSplit[nextPathSplit.length] === '/')
|
|
487
|
+
|
|
488
|
+
if (!pathIsFuzzyEqual) {
|
|
489
|
+
return false
|
|
517
490
|
}
|
|
491
|
+
}
|
|
518
492
|
|
|
519
|
-
|
|
520
|
-
|
|
493
|
+
if (activeOptions?.includeSearch ?? true) {
|
|
494
|
+
const searchTest = deepEqual(currentLocation.search, next.search, {
|
|
495
|
+
partial: !activeOptions?.exact,
|
|
496
|
+
ignoreUndefined: !activeOptions?.explicitUndefined,
|
|
497
|
+
})
|
|
498
|
+
if (!searchTest) {
|
|
499
|
+
return false
|
|
521
500
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (activeOptions?.includeHash) {
|
|
504
|
+
return isHydrated && currentLocation.hash === next.hash
|
|
505
|
+
}
|
|
506
|
+
return true
|
|
507
|
+
}, [
|
|
508
|
+
activeOptions?.exact,
|
|
509
|
+
activeOptions?.explicitUndefined,
|
|
510
|
+
activeOptions?.includeHash,
|
|
511
|
+
activeOptions?.includeSearch,
|
|
512
|
+
currentLocation,
|
|
513
|
+
externalLink,
|
|
514
|
+
isHydrated,
|
|
515
|
+
next.hash,
|
|
516
|
+
next.pathname,
|
|
517
|
+
next.search,
|
|
518
|
+
router.basepath,
|
|
519
|
+
])
|
|
525
520
|
|
|
526
521
|
// Get the active props
|
|
527
522
|
const resolvedActiveProps: React.HTMLAttributes<HTMLAnchorElement> = isActive
|
package/src/not-found.tsx
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
2
|
import { isNotFound } from '@tanstack/router-core'
|
|
3
|
+
import { isServer } from '@tanstack/router-core/isServer'
|
|
4
|
+
import { useStore } from '@tanstack/react-store'
|
|
3
5
|
import { CatchBoundary } from './CatchBoundary'
|
|
4
|
-
import {
|
|
6
|
+
import { useRouter } from './useRouter'
|
|
5
7
|
import type { ErrorInfo } from 'react'
|
|
6
8
|
import type { NotFoundError } from '@tanstack/router-core'
|
|
7
9
|
|
|
@@ -10,10 +12,45 @@ export function CatchNotFound(props: {
|
|
|
10
12
|
onCatch?: (error: Error, errorInfo: ErrorInfo) => void
|
|
11
13
|
children: React.ReactNode
|
|
12
14
|
}) {
|
|
15
|
+
const router = useRouter()
|
|
16
|
+
|
|
17
|
+
if (isServer ?? router.isServer) {
|
|
18
|
+
const pathname = router.stores.location.state.pathname
|
|
19
|
+
const status = router.stores.status.state
|
|
20
|
+
const resetKey = `not-found-${pathname}-${status}`
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<CatchBoundary
|
|
24
|
+
getResetKey={() => resetKey}
|
|
25
|
+
onCatch={(error, errorInfo) => {
|
|
26
|
+
if (isNotFound(error)) {
|
|
27
|
+
props.onCatch?.(error, errorInfo)
|
|
28
|
+
} else {
|
|
29
|
+
throw error
|
|
30
|
+
}
|
|
31
|
+
}}
|
|
32
|
+
errorComponent={({ error }) => {
|
|
33
|
+
if (isNotFound(error)) {
|
|
34
|
+
return props.fallback?.(error)
|
|
35
|
+
} else {
|
|
36
|
+
throw error
|
|
37
|
+
}
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
{props.children}
|
|
41
|
+
</CatchBoundary>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
13
45
|
// TODO: Some way for the user to programmatically reset the not-found boundary?
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
46
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static
|
|
47
|
+
const pathname = useStore(
|
|
48
|
+
router.stores.location,
|
|
49
|
+
(location) => location.pathname,
|
|
50
|
+
)
|
|
51
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks -- condition is static
|
|
52
|
+
const status = useStore(router.stores.status, (status) => status)
|
|
53
|
+
const resetKey = `not-found-${pathname}-${status}`
|
|
17
54
|
|
|
18
55
|
return (
|
|
19
56
|
<CatchBoundary
|
package/src/router.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { RouterCore } from '@tanstack/router-core'
|
|
2
2
|
import { createFileRoute, createLazyFileRoute } from './fileRoute'
|
|
3
|
+
import { getStoreFactory } from './routerStores'
|
|
3
4
|
import type { RouterHistory } from '@tanstack/history'
|
|
4
5
|
import type {
|
|
5
6
|
AnyRoute,
|
|
@@ -114,7 +115,7 @@ export class Router<
|
|
|
114
115
|
TDehydrated
|
|
115
116
|
>,
|
|
116
117
|
) {
|
|
117
|
-
super(options)
|
|
118
|
+
super(options, getStoreFactory)
|
|
118
119
|
}
|
|
119
120
|
}
|
|
120
121
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { batch, createStore } from '@tanstack/react-store'
|
|
2
|
+
import {
|
|
3
|
+
createNonReactiveMutableStore,
|
|
4
|
+
createNonReactiveReadonlyStore,
|
|
5
|
+
} from '@tanstack/router-core'
|
|
6
|
+
import { isServer } from '@tanstack/router-core/isServer'
|
|
7
|
+
import type { Readable } from '@tanstack/react-store'
|
|
8
|
+
import type { GetStoreConfig } from '@tanstack/router-core'
|
|
9
|
+
|
|
10
|
+
declare module '@tanstack/router-core' {
|
|
11
|
+
export interface RouterReadableStore<TValue> extends Readable<TValue> {}
|
|
12
|
+
}
|
|
13
|
+
export const getStoreFactory: GetStoreConfig = (opts) => {
|
|
14
|
+
if (isServer ?? opts.isServer) {
|
|
15
|
+
return {
|
|
16
|
+
createMutableStore: createNonReactiveMutableStore,
|
|
17
|
+
createReadonlyStore: createNonReactiveReadonlyStore,
|
|
18
|
+
batch: (fn) => fn(),
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
createMutableStore: createStore,
|
|
23
|
+
createReadonlyStore: createStore,
|
|
24
|
+
batch: batch,
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/ssr/RouterClient.tsx
CHANGED
|
@@ -7,7 +7,7 @@ let hydrationPromise: Promise<void | Array<Array<void>>> | undefined
|
|
|
7
7
|
|
|
8
8
|
export function RouterClient(props: { router: AnyRouter }) {
|
|
9
9
|
if (!hydrationPromise) {
|
|
10
|
-
if (!props.router.state.
|
|
10
|
+
if (!props.router.stores.matchesId.state.length) {
|
|
11
11
|
hydrationPromise = hydrate(props.router)
|
|
12
12
|
} else {
|
|
13
13
|
hydrationPromise = Promise.resolve()
|