@tanstack/react-router 0.0.1-beta.230 → 0.0.1-beta.232

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tanstack/react-router",
3
3
  "author": "Tanner Linsley",
4
- "version": "0.0.1-beta.230",
4
+ "version": "0.0.1-beta.232",
5
5
  "license": "MIT",
6
6
  "repository": "tanstack/router",
7
7
  "homepage": "https://tanstack.com/router",
@@ -42,7 +42,7 @@
42
42
  "@babel/runtime": "^7.16.7",
43
43
  "tiny-invariant": "^1.3.1",
44
44
  "tiny-warning": "^1.0.3",
45
- "@tanstack/history": "0.0.1-beta.230"
45
+ "@tanstack/history": "0.0.1-beta.232"
46
46
  },
47
47
  "scripts": {
48
48
  "build": "rollup --config rollup.config.js"
package/src/Matches.tsx CHANGED
@@ -94,7 +94,9 @@ export function Match({ matches }: { matches: RouteMatch[] }) {
94
94
  const match = matches[0]!
95
95
  const routeId = match?.routeId
96
96
  const route = routesById[routeId]!
97
- const locationKey = useRouterState().location.state?.key
97
+ const router = useRouter()
98
+ const locationKey = router.latestLocation.state?.key
99
+ // const locationKey = useRouterState().location.state?.key
98
100
 
99
101
  const PendingComponent = (route.options.pendingComponent ??
100
102
  options.defaultPendingComponent) as any
@@ -18,7 +18,7 @@ import {
18
18
  RouterOptions,
19
19
  RouterState,
20
20
  } from './router'
21
- import { NoInfer, PickAsRequired } from './utils'
21
+ import { NoInfer, PickAsRequired, useLayoutEffect } from './utils'
22
22
  import { MatchRouteOptions } from './Matches'
23
23
  import { RouteMatch } from './Matches'
24
24
 
@@ -72,48 +72,42 @@ if (typeof document !== 'undefined') {
72
72
  window.__TSR_ROUTER_CONTEXT__ = routerContext as any
73
73
  }
74
74
 
75
- export class SearchParamError extends Error {}
76
-
77
- export class PathParamError extends Error {}
78
-
79
- export function getInitialRouterState(
80
- location: ParsedLocation,
81
- ): RouterState<any> {
82
- return {
83
- status: 'idle',
84
- resolvedLocation: location,
85
- location,
86
- matches: [],
87
- pendingMatches: [],
88
- lastUpdated: Date.now(),
89
- }
90
- }
91
-
92
75
  export function RouterProvider<
93
76
  TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
94
77
  TDehydrated extends Record<string, any> = Record<string, any>,
95
78
  >({ router, ...rest }: RouterProps<TRouteTree, TDehydrated>) {
96
79
  // Allow the router to update options on the router instance
97
- router.updateOptions({
80
+ router.update({
98
81
  ...router.options,
99
82
  ...rest,
100
-
101
83
  context: {
102
84
  ...router.options.context,
103
85
  ...rest?.context,
104
86
  },
105
- } as PickAsRequired<
106
- RouterOptions<TRouteTree, TDehydrated>,
107
- 'stringifySearch' | 'parseSearch' | 'context'
108
- >)
87
+ } as any)
88
+
89
+ const inner = <RouterProviderInner<TRouteTree, TDehydrated> router={router} />
90
+
91
+ if (router.options.Wrap) {
92
+ return <router.options.Wrap>{inner}</router.options.Wrap>
93
+ }
94
+
95
+ return inner
96
+ }
109
97
 
98
+ function RouterProviderInner<
99
+ TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
100
+ TDehydrated extends Record<string, any> = Record<string, any>,
101
+ >({ router }: RouterProps<TRouteTree, TDehydrated>) {
110
102
  const [preState, setState] = React.useState(() => router.state)
111
103
  const [isTransitioning, startReactTransition] = React.useTransition()
104
+ const isAnyTransitioning =
105
+ isTransitioning || preState.matches.some((d) => d.status === 'pending')
112
106
 
113
107
  const state = React.useMemo<RouterState<TRouteTree>>(
114
108
  () => ({
115
109
  ...preState,
116
- status: isTransitioning ? 'pending' : 'idle',
110
+ status: isAnyTransitioning ? 'pending' : 'idle',
117
111
  location: isTransitioning ? router.latestLocation : preState.location,
118
112
  pendingMatches: router.pendingMatches,
119
113
  }),
@@ -124,19 +118,22 @@ export function RouterProvider<
124
118
  router.state = state
125
119
  router.startReactTransition = startReactTransition
126
120
 
127
- React.useLayoutEffect(() => {
121
+ const tryLoad = () => {
122
+ if (state.location !== router.latestLocation) {
123
+ startReactTransition(() => {
124
+ try {
125
+ router.load()
126
+ } catch (err) {
127
+ console.error(err)
128
+ }
129
+ })
130
+ }
131
+ }
132
+
133
+ useLayoutEffect(() => {
128
134
  const unsub = router.history.subscribe(() => {
129
135
  router.latestLocation = router.parseLocation(router.latestLocation)
130
-
131
- if (state.location !== router.latestLocation) {
132
- startReactTransition(() => {
133
- try {
134
- router.load()
135
- } catch (err) {
136
- console.error(err)
137
- }
138
- })
139
- }
136
+ tryLoad()
140
137
  })
141
138
 
142
139
  const nextLocation = router.buildLocation({
@@ -155,7 +152,7 @@ export function RouterProvider<
155
152
  }
156
153
  }, [router.history])
157
154
 
158
- React.useLayoutEffect(() => {
155
+ useLayoutEffect(() => {
159
156
  if (!isTransitioning && state.resolvedLocation !== state.location) {
160
157
  router.emit({
161
158
  type: 'onResolved',
@@ -172,14 +169,10 @@ export function RouterProvider<
172
169
  }
173
170
  })
174
171
 
175
- React.useLayoutEffect(() => {
176
- startReactTransition(() => {
177
- try {
178
- router.load()
179
- } catch (err) {
180
- console.error(err)
181
- }
182
- })
172
+ useLayoutEffect(() => {
173
+ if (!window.__TSR_DEHYDRATED__) {
174
+ tryLoad()
175
+ }
183
176
  }, [])
184
177
 
185
178
  return (
@@ -217,7 +210,10 @@ export type RouterProps<
217
210
  export function useRouter<
218
211
  TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
219
212
  >(): Router<TRouteTree> {
220
- const resolvedContext = window.__TSR_ROUTER_CONTEXT__ || routerContext
213
+ const resolvedContext =
214
+ typeof document !== 'undefined'
215
+ ? window.__TSR_ROUTER_CONTEXT__ || routerContext
216
+ : routerContext
221
217
  const value = React.useContext(resolvedContext)
222
218
  warning(value, 'useRouter must be used inside a <RouterProvider> component!')
223
219
  return value as any
package/src/awaited.tsx CHANGED
@@ -18,7 +18,7 @@ export function useAwaited<T>({ promise }: AwaitOptions<T>): [T] {
18
18
  }
19
19
 
20
20
  if (state.status === 'pending') {
21
- throw promise
21
+ throw new Promise((r) => setTimeout(r, 1)).then(() => promise)
22
22
  }
23
23
 
24
24
  if (state.status === 'error') {
package/src/router.ts CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  HistoryState,
4
4
  RouterHistory,
5
5
  createBrowserHistory,
6
+ createMemoryHistory,
6
7
  } from '@tanstack/history'
7
8
 
8
9
  //
@@ -28,6 +29,7 @@ import {
28
29
  functionalUpdate,
29
30
  last,
30
31
  pick,
32
+ PickAsPartial,
31
33
  } from './utils'
32
34
  import {
33
35
  ErrorRouteComponent,
@@ -45,11 +47,9 @@ import {
45
47
  InjectedHtmlEntry,
46
48
  MatchRouteFn,
47
49
  NavigateFn,
48
- PathParamError,
49
- SearchParamError,
50
- getInitialRouterState,
51
50
  getRouteMatch,
52
51
  } from './RouterProvider'
52
+
53
53
  import {
54
54
  cleanPath,
55
55
  interpolatePath,
@@ -62,7 +62,7 @@ import {
62
62
  } from './path'
63
63
  import invariant from 'tiny-invariant'
64
64
  import { isRedirect } from './redirects'
65
- import warning from 'tiny-warning'
65
+ // import warning from 'tiny-warning'
66
66
 
67
67
  //
68
68
 
@@ -125,10 +125,11 @@ export interface RouterOptions<
125
125
  routeTree?: TRouteTree
126
126
  basepath?: string
127
127
  context?: TRouteTree['types']['routerContext']
128
- // dehydrate?: () => TDehydrated
129
- // hydrate?: (dehydrated: TDehydrated) => void
128
+ dehydrate?: () => TDehydrated
129
+ hydrate?: (dehydrated: TDehydrated) => void
130
130
  routeMasks?: RouteMask<TRouteTree>[]
131
131
  unmaskOnReload?: boolean
132
+ Wrap?: (props: { children: any }) => JSX.Element
132
133
  }
133
134
 
134
135
  export interface RouterState<TRouteTree extends AnyRoute = AnyRoute> {
@@ -249,7 +250,7 @@ export class Router<
249
250
  flatRoutes!: AnyRoute[]
250
251
 
251
252
  constructor(options: RouterConstructorOptions<TRouteTree, TDehydrated>) {
252
- this.updateOptions({
253
+ this.update({
253
254
  defaultPreloadDelay: 50,
254
255
  defaultPendingMs: 1000,
255
256
  defaultPendingMinMs: 500,
@@ -260,28 +261,17 @@ export class Router<
260
261
  })
261
262
  }
262
263
 
263
- startReactTransition: (fn: () => void) => void = () => {
264
- warning(
265
- false,
266
- 'startReactTransition implementation is missing. If you see this, please file an issue.',
267
- )
268
- }
269
-
270
- setState: (
271
- fn: (s: RouterState<TRouteTree>) => RouterState<TRouteTree>,
272
- ) => void = () => {
273
- warning(
274
- false,
275
- 'setState implementation is missing. If you see this, please file an issue.',
276
- )
264
+ // These are default implementations that can optionally be overridden
265
+ // by the router provider once rendered. We provide these so that the
266
+ // router can be used in a non-react environment if necessary
267
+ startReactTransition: (fn: () => void) => void = (fn) => fn()
268
+ setState: (updater: NonNullableUpdater<RouterState<TRouteTree>>) => void = (
269
+ updater,
270
+ ) => {
271
+ this.state = functionalUpdate(updater, this.state)
277
272
  }
278
273
 
279
- updateOptions = (
280
- newOptions: PickAsRequired<
281
- RouterOptions<TRouteTree, TDehydrated>,
282
- 'stringifySearch' | 'parseSearch' | 'context'
283
- >,
284
- ) => {
274
+ update = (newOptions: RouterConstructorOptions<TRouteTree, TDehydrated>) => {
285
275
  this.options = {
286
276
  ...this.options,
287
277
  ...newOptions,
@@ -293,7 +283,11 @@ export class Router<
293
283
  !this.history ||
294
284
  (this.options.history && this.options.history !== this.history)
295
285
  ) {
296
- this.history = this.options.history ?? createBrowserHistory()
286
+ this.history =
287
+ this.options.history ??
288
+ (typeof document !== 'undefined'
289
+ ? createBrowserHistory()
290
+ : createMemoryHistory())
297
291
  this.latestLocation = this.parseLocation()
298
292
  }
299
293
 
@@ -1168,6 +1162,8 @@ export class Router<
1168
1162
  // forcefully show the pending component
1169
1163
  if (pendingPromise) {
1170
1164
  pendingPromise.then(() => {
1165
+ if ((latestPromise = checkLatest())) return
1166
+
1171
1167
  didShowPending = true
1172
1168
  matches[index] = match = {
1173
1169
  ...match,
@@ -1195,6 +1191,8 @@ export class Router<
1195
1191
  await new Promise((r) => setTimeout(r, pendingMinMs))
1196
1192
  }
1197
1193
 
1194
+ if ((latestPromise = checkLatest())) return await latestPromise
1195
+
1198
1196
  matches[index] = match = {
1199
1197
  ...match,
1200
1198
  error: undefined,
@@ -1279,7 +1277,7 @@ export class Router<
1279
1277
  // Ingest the new matches
1280
1278
  this.setState((s) => ({
1281
1279
  ...s,
1282
- status: 'pending',
1280
+ // status: 'pending',
1283
1281
  location: next,
1284
1282
  matches,
1285
1283
  }))
@@ -1316,6 +1314,7 @@ export class Router<
1316
1314
  // ...s,
1317
1315
  // status: 'idle',
1318
1316
  // resolvedLocation: s.location,
1317
+ // matches,
1319
1318
  // }))
1320
1319
 
1321
1320
  //
@@ -1578,62 +1577,69 @@ export class Router<
1578
1577
  return undefined
1579
1578
  }
1580
1579
 
1581
- // dehydrate = (): DehydratedRouter => {
1582
- // return {
1583
- // state: {
1584
- // dehydratedMatches: this.state.matches.map((d) =>
1585
- // pick(d, ['fetchedAt', 'invalid', 'id', 'status', 'updatedAt']),
1586
- // ),
1587
- // },
1588
- // }
1589
- // }
1580
+ dehydrate = (): DehydratedRouter => {
1581
+ return {
1582
+ state: {
1583
+ dehydratedMatches: this.state.matches.map((d) =>
1584
+ pick(d, [
1585
+ 'fetchedAt',
1586
+ 'invalid',
1587
+ 'id',
1588
+ 'status',
1589
+ 'updatedAt',
1590
+ 'loaderData',
1591
+ ]),
1592
+ ),
1593
+ },
1594
+ }
1595
+ }
1590
1596
 
1591
- // hydrate = async (__do_not_use_server_ctx?: HydrationCtx) => {
1592
- // let _ctx = __do_not_use_server_ctx
1593
- // // Client hydrates from window
1594
- // if (typeof document !== 'undefined') {
1595
- // _ctx = window.__TSR_DEHYDRATED__
1596
- // }
1597
-
1598
- // invariant(
1599
- // _ctx,
1600
- // 'Expected to find a __TSR_DEHYDRATED__ property on window... but we did not. Did you forget to render <DehydrateRouter /> in your app?',
1601
- // )
1602
-
1603
- // const ctx = _ctx
1604
- // this.dehydratedData = ctx.payload as any
1605
- // this.options.hydrate?.(ctx.payload as any)
1606
- // const dehydratedState = ctx.router.state
1607
-
1608
- // let matches = this.matchRoutes(
1609
- // this.state.location.pathname,
1610
- // this.state.location.search,
1611
- // ).map((match) => {
1612
- // const dehydratedMatch = dehydratedState.dehydratedMatches.find(
1613
- // (d) => d.id === match.id,
1614
- // )
1615
-
1616
- // invariant(
1617
- // dehydratedMatch,
1618
- // `Could not find a client-side match for dehydrated match with id: ${match.id}!`,
1619
- // )
1620
-
1621
- // if (dehydratedMatch) {
1622
- // return {
1623
- // ...match,
1624
- // ...dehydratedMatch,
1625
- // }
1626
- // }
1627
- // return match
1628
- // })
1629
-
1630
- // this.setState((s) => {
1631
- // return {
1632
- // ...s,
1633
- // matches: dehydratedState.dehydratedMatches as any,
1634
- // }
1635
- // })
1636
- // }
1597
+ hydrate = async (__do_not_use_server_ctx?: HydrationCtx) => {
1598
+ let _ctx = __do_not_use_server_ctx
1599
+ // Client hydrates from window
1600
+ if (typeof document !== 'undefined') {
1601
+ _ctx = window.__TSR_DEHYDRATED__
1602
+ }
1603
+
1604
+ invariant(
1605
+ _ctx,
1606
+ 'Expected to find a __TSR_DEHYDRATED__ property on window... but we did not. Did you forget to render <DehydrateRouter /> in your app?',
1607
+ )
1608
+
1609
+ const ctx = _ctx
1610
+ this.dehydratedData = ctx.payload as any
1611
+ this.options.hydrate?.(ctx.payload as any)
1612
+ const dehydratedState = ctx.router.state
1613
+
1614
+ let matches = this.matchRoutes(
1615
+ this.state.location.pathname,
1616
+ this.state.location.search,
1617
+ ).map((match) => {
1618
+ const dehydratedMatch = dehydratedState.dehydratedMatches.find(
1619
+ (d) => d.id === match.id,
1620
+ )
1621
+
1622
+ invariant(
1623
+ dehydratedMatch,
1624
+ `Could not find a client-side match for dehydrated match with id: ${match.id}!`,
1625
+ )
1626
+
1627
+ if (dehydratedMatch) {
1628
+ return {
1629
+ ...match,
1630
+ ...dehydratedMatch,
1631
+ }
1632
+ }
1633
+ return match
1634
+ })
1635
+
1636
+ this.setState((s) => {
1637
+ return {
1638
+ ...s,
1639
+ matches: matches as any,
1640
+ }
1641
+ })
1642
+ }
1637
1643
 
1638
1644
  // resolveMatchPromise = (matchId: string, key: string, value: any) => {
1639
1645
  // state.matches
@@ -1658,3 +1664,19 @@ export function lazyFn<
1658
1664
  function isCtrlEvent(e: MouseEvent) {
1659
1665
  return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey)
1660
1666
  }
1667
+ export class SearchParamError extends Error {}
1668
+
1669
+ export class PathParamError extends Error {}
1670
+
1671
+ export function getInitialRouterState(
1672
+ location: ParsedLocation,
1673
+ ): RouterState<any> {
1674
+ return {
1675
+ status: 'idle',
1676
+ resolvedLocation: location,
1677
+ location,
1678
+ matches: [],
1679
+ pendingMatches: [],
1680
+ lastUpdated: Date.now(),
1681
+ }
1682
+ }
@@ -23,10 +23,26 @@ type Cache = {
23
23
  set: (updater: NonNullableUpdater<CacheState>) => void
24
24
  }
25
25
 
26
- let cache: Cache
27
-
28
26
  const sessionsStorage = typeof window !== 'undefined' && window.sessionStorage
29
27
 
28
+ let cache: Cache = sessionsStorage
29
+ ? (() => {
30
+ const storageKey = 'tsr-scroll-restoration-v2'
31
+
32
+ const state: CacheState = JSON.parse(
33
+ window.sessionStorage.getItem(storageKey) || 'null',
34
+ ) || { cached: {}, next: {} }
35
+
36
+ return {
37
+ state,
38
+ set: (updater) => {
39
+ cache.state = functionalUpdate(updater, cache.state)
40
+ window.sessionStorage.setItem(storageKey, JSON.stringify(cache.state))
41
+ },
42
+ }
43
+ })()
44
+ : (undefined as any)
45
+
30
46
  export type ScrollRestorationOptions = {
31
47
  getKey?: (location: ParsedLocation) => string
32
48
  }
@@ -39,29 +55,6 @@ export function useScrollRestoration(options?: ScrollRestorationOptions) {
39
55
  useLayoutEffect(() => {
40
56
  const getKey = options?.getKey || defaultGetKey
41
57
 
42
- if (sessionsStorage) {
43
- if (!cache) {
44
- cache = (() => {
45
- const storageKey = 'tsr-scroll-restoration-v2'
46
-
47
- const state: CacheState = JSON.parse(
48
- window.sessionStorage.getItem(storageKey) || 'null',
49
- ) || { cached: {}, next: {} }
50
-
51
- return {
52
- state,
53
- set: (updater) => {
54
- cache.state = functionalUpdate(updater, cache.state)
55
- window.sessionStorage.setItem(
56
- storageKey,
57
- JSON.stringify(cache.state),
58
- )
59
- },
60
- }
61
- })()
62
- }
63
- }
64
-
65
58
  const { history } = window
66
59
  if (history.scrollRestoration) {
67
60
  history.scrollRestoration = 'manual'
@@ -71,10 +64,21 @@ export function useScrollRestoration(options?: ScrollRestorationOptions) {
71
64
  if (weakScrolledElements.has(event.target)) return
72
65
  weakScrolledElements.add(event.target)
73
66
 
74
- const elementSelector =
75
- event.target === document || event.target === window
76
- ? windowKey
77
- : getCssSelector(event.target)
67
+ let elementSelector = ''
68
+
69
+ if (event.target === document || event.target === window) {
70
+ elementSelector = windowKey
71
+ } else {
72
+ const attrId = (event.target as Element).getAttribute(
73
+ 'data-scroll-restoration-id',
74
+ )
75
+
76
+ if (attrId) {
77
+ elementSelector = `[data-scroll-restoration-id="${attrId}"]`
78
+ } else {
79
+ elementSelector = getCssSelector(event.target)
80
+ }
81
+ }
78
82
 
79
83
  if (!cache.state.next[elementSelector]) {
80
84
  cache.set((c) => ({
@@ -90,20 +94,6 @@ export function useScrollRestoration(options?: ScrollRestorationOptions) {
90
94
  }
91
95
  }
92
96
 
93
- const getCssSelector = (el: any): string => {
94
- let path = [],
95
- parent
96
- while ((parent = el.parentNode)) {
97
- path.unshift(
98
- `${el.tagName}:nth-child(${
99
- ([].indexOf as any).call(parent.children, el) + 1
100
- })`,
101
- )
102
- el = parent
103
- }
104
- return `${path.join(' > ')}`.toLowerCase()
105
- }
106
-
107
97
  if (typeof document !== 'undefined') {
108
98
  document.addEventListener('scroll', onScroll, true)
109
99
  }
@@ -190,3 +180,51 @@ export function ScrollRestoration(props: ScrollRestorationOptions) {
190
180
  useScrollRestoration(props)
191
181
  return null
192
182
  }
183
+
184
+ export function useElementScrollRestoration(
185
+ options: (
186
+ | {
187
+ id: string
188
+ getElement?: () => Element | undefined | null
189
+ }
190
+ | {
191
+ id?: string
192
+ getElement: () => Element | undefined | null
193
+ }
194
+ ) & {
195
+ getKey?: (location: ParsedLocation) => string
196
+ },
197
+ ) {
198
+ const router = useRouter()
199
+ const getKey = options?.getKey || defaultGetKey
200
+
201
+ let elementSelector = ''
202
+
203
+ if (options.id) {
204
+ elementSelector = `[data-scroll-restoration-id="${options.id}"]`
205
+ } else {
206
+ const element = options.getElement?.()
207
+ if (!element) {
208
+ return
209
+ }
210
+ elementSelector = getCssSelector(element)
211
+ }
212
+
213
+ const restoreKey = getKey(router.latestLocation)
214
+ const cacheKey = [restoreKey, elementSelector].join(delimiter)
215
+ return cache.state.cached[cacheKey]
216
+ }
217
+
218
+ function getCssSelector(el: any): string {
219
+ let path = [],
220
+ parent
221
+ while ((parent = el.parentNode)) {
222
+ path.unshift(
223
+ `${el.tagName}:nth-child(${
224
+ ([].indexOf as any).call(parent.children, el) + 1
225
+ })`,
226
+ )
227
+ el = parent
228
+ }
229
+ return `${path.join(' > ')}`.toLowerCase()
230
+ }