@tanstack/react-router 0.0.1-alpha.1 → 0.0.1-alpha.10

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-alpha.1",
4
+ "version": "0.0.1-alpha.10",
5
5
  "license": "MIT",
6
6
  "repository": "tanstack/router",
7
7
  "homepage": "https://react-router.tanstack.com/",
@@ -31,7 +31,7 @@
31
31
  "node": ">=12"
32
32
  },
33
33
  "files": [
34
- "build",
34
+ "build/**",
35
35
  "src"
36
36
  ],
37
37
  "peerDependencies": {
@@ -40,7 +40,7 @@
40
40
  },
41
41
  "dependencies": {
42
42
  "@babel/runtime": "^7.16.7",
43
- "@tanstack/router-core": "0.0.1-alpha.1",
43
+ "@tanstack/router-core": "0.0.1-alpha.10",
44
44
  "use-sync-external-store": "^1.2.0"
45
45
  },
46
46
  "devDependencies": {
package/src/index.tsx CHANGED
@@ -6,8 +6,8 @@ import {
6
6
  AnyRoute,
7
7
  RootRouteId,
8
8
  rootRouteId,
9
- Route,
10
9
  Router,
10
+ RouterState,
11
11
  } from '@tanstack/router-core'
12
12
  import {
13
13
  warning,
@@ -29,6 +29,7 @@ import {
29
29
  ResolveRelativePath,
30
30
  NoInfer,
31
31
  ToOptions,
32
+ invariant,
32
33
  } from '@tanstack/router-core'
33
34
 
34
35
  export * from '@tanstack/router-core'
@@ -49,40 +50,41 @@ declare module '@tanstack/router-core' {
49
50
  Route<TAllRouteInfo, TAllRouteInfo['routeInfoById'][RootRouteId]>,
50
51
  'linkProps' | 'Link' | 'MatchRoute'
51
52
  > {
53
+ useState: () => RouterState
52
54
  useRoute: <TId extends keyof TAllRouteInfo['routeInfoById']>(
53
55
  routeId: TId,
54
56
  ) => Route<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TId]>
55
57
  useMatch: <TId extends keyof TAllRouteInfo['routeInfoById']>(
56
58
  routeId: TId,
57
59
  ) => RouteMatch<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TId]>
58
- // linkProps: <TTo extends string = '.'>(
59
- // props: LinkPropsOptions<TAllRouteInfo, '/', TTo> &
60
- // React.AnchorHTMLAttributes<HTMLAnchorElement>,
61
- // ) => React.AnchorHTMLAttributes<HTMLAnchorElement>
62
- // Link: <TTo extends string = '.'>(
63
- // props: LinkPropsOptions<TAllRouteInfo, '/', TTo> &
64
- // React.AnchorHTMLAttributes<HTMLAnchorElement> &
65
- // Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'children'> & {
66
- // // If a function is passed as a child, it will be given the `isActive` boolean to aid in further styling on the element it returns
67
- // children?:
68
- // | React.ReactNode
69
- // | ((state: { isActive: boolean }) => React.ReactNode)
70
- // },
71
- // ) => JSX.Element
72
- // MatchRoute: <TTo extends string = '.'>(
73
- // props: ToOptions<TAllRouteInfo, '/', TTo> &
74
- // MatchRouteOptions & {
75
- // // If a function is passed as a child, it will be given the `isActive` boolean to aid in further styling on the element it returns
76
- // children?:
77
- // | React.ReactNode
78
- // | ((
79
- // params: RouteInfoByPath<
80
- // TAllRouteInfo,
81
- // ResolveRelativePath<'/', NoInfer<TTo>>
82
- // >['allParams'],
83
- // ) => React.ReactNode)
84
- // },
85
- // ) => JSX.Element
60
+ linkProps: <TTo extends string = '.'>(
61
+ props: LinkPropsOptions<TAllRouteInfo, '/', TTo> &
62
+ React.AnchorHTMLAttributes<HTMLAnchorElement>,
63
+ ) => React.AnchorHTMLAttributes<HTMLAnchorElement>
64
+ Link: <TTo extends string = '.'>(
65
+ props: LinkPropsOptions<TAllRouteInfo, '/', TTo> &
66
+ React.AnchorHTMLAttributes<HTMLAnchorElement> &
67
+ Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'children'> & {
68
+ // If a function is passed as a child, it will be given the `isActive` boolean to aid in further styling on the element it returns
69
+ children?:
70
+ | React.ReactNode
71
+ | ((state: { isActive: boolean }) => React.ReactNode)
72
+ },
73
+ ) => JSX.Element
74
+ MatchRoute: <TTo extends string = '.'>(
75
+ props: ToOptions<TAllRouteInfo, '/', TTo> &
76
+ MatchRouteOptions & {
77
+ // If a function is passed as a child, it will be given the `isActive` boolean to aid in further styling on the element it returns
78
+ children?:
79
+ | React.ReactNode
80
+ | ((
81
+ params: RouteInfoByPath<
82
+ TAllRouteInfo,
83
+ ResolveRelativePath<'/', NoInfer<TTo>>
84
+ >['allParams'],
85
+ ) => React.ReactNode)
86
+ },
87
+ ) => JSX.Element
86
88
  }
87
89
 
88
90
  interface Route<
@@ -174,201 +176,211 @@ const useRouterSubscription = (router: Router<any, any>) => {
174
176
  export function createReactRouter<
175
177
  TRouteConfig extends AnyRouteConfig = RouteConfig,
176
178
  >(opts: RouterOptions<TRouteConfig>): Router<TRouteConfig> {
179
+ const makeRouteExt = (
180
+ route: AnyRoute,
181
+ router: Router<any, any>,
182
+ ): Pick<AnyRoute, 'linkProps' | 'Link' | 'MatchRoute'> => {
183
+ return {
184
+ linkProps: (options) => {
185
+ const {
186
+ // custom props
187
+ type,
188
+ children,
189
+ target,
190
+ activeProps = () => ({ className: 'active' }),
191
+ inactiveProps = () => ({}),
192
+ activeOptions,
193
+ disabled,
194
+ // fromCurrent,
195
+ hash,
196
+ search,
197
+ params,
198
+ to,
199
+ preload,
200
+ preloadDelay,
201
+ preloadMaxAge,
202
+ replace,
203
+ // element props
204
+ style,
205
+ className,
206
+ onClick,
207
+ onFocus,
208
+ onMouseEnter,
209
+ onMouseLeave,
210
+ onTouchStart,
211
+ onTouchEnd,
212
+ ...rest
213
+ } = options
214
+
215
+ const linkInfo = route.buildLink(options)
216
+
217
+ if (linkInfo.type === 'external') {
218
+ const { href } = linkInfo
219
+ return { href }
220
+ }
221
+
222
+ const {
223
+ handleClick,
224
+ handleFocus,
225
+ handleEnter,
226
+ handleLeave,
227
+ isActive,
228
+ next,
229
+ } = linkInfo
230
+
231
+ const composeHandlers =
232
+ (handlers: (undefined | ((e: any) => void))[]) =>
233
+ (e: React.SyntheticEvent) => {
234
+ e.persist()
235
+ handlers.forEach((handler) => {
236
+ if (handler) handler(e)
237
+ })
238
+ }
239
+
240
+ // Get the active props
241
+ const resolvedActiveProps: React.HTMLAttributes<HTMLAnchorElement> =
242
+ isActive ? functionalUpdate(activeProps, {}) ?? {} : {}
243
+
244
+ // Get the inactive props
245
+ const resolvedInactiveProps: React.HTMLAttributes<HTMLAnchorElement> =
246
+ isActive ? {} : functionalUpdate(inactiveProps, {}) ?? {}
247
+
248
+ return {
249
+ ...resolvedActiveProps,
250
+ ...resolvedInactiveProps,
251
+ ...rest,
252
+ href: disabled ? undefined : next.href,
253
+ onClick: composeHandlers([handleClick, onClick]),
254
+ onFocus: composeHandlers([handleFocus, onFocus]),
255
+ onMouseEnter: composeHandlers([handleEnter, onMouseEnter]),
256
+ onMouseLeave: composeHandlers([handleLeave, onMouseLeave]),
257
+ target,
258
+ style: {
259
+ ...style,
260
+ ...resolvedActiveProps.style,
261
+ ...resolvedInactiveProps.style,
262
+ },
263
+ className:
264
+ [
265
+ className,
266
+ resolvedActiveProps.className,
267
+ resolvedInactiveProps.className,
268
+ ]
269
+ .filter(Boolean)
270
+ .join(' ') || undefined,
271
+ ...(disabled
272
+ ? {
273
+ role: 'link',
274
+ 'aria-disabled': true,
275
+ }
276
+ : undefined),
277
+ ['data-status']: isActive ? 'active' : undefined,
278
+ }
279
+ },
280
+ Link: React.forwardRef((props: any, ref) => {
281
+ const linkProps = route.linkProps(props)
282
+
283
+ useRouterSubscription(router)
284
+
285
+ return (
286
+ <a
287
+ {...{
288
+ ref: ref as any,
289
+ ...linkProps,
290
+ children:
291
+ typeof props.children === 'function'
292
+ ? props.children({
293
+ isActive: (linkProps as any)['data-status'] === 'active',
294
+ })
295
+ : props.children,
296
+ }}
297
+ />
298
+ )
299
+ }) as any,
300
+ MatchRoute: (opts) => {
301
+ const { pending, caseSensitive, children, ...rest } = opts
302
+
303
+ const params = route.matchRoute(rest as any, {
304
+ pending,
305
+ caseSensitive,
306
+ })
307
+
308
+ if (!params) {
309
+ return null
310
+ }
311
+
312
+ return typeof opts.children === 'function'
313
+ ? opts.children(params as any)
314
+ : (opts.children as any)
315
+ },
316
+ }
317
+ }
318
+
177
319
  const coreRouter = createRouter<TRouteConfig>({
178
320
  ...opts,
179
321
  createRouter: (router) => {
180
- const routerExt: Pick<Router<any, any>, 'useRoute' | 'useMatch'> = {
322
+ const routerExt: Pick<
323
+ Router<any, any>,
324
+ 'useRoute' | 'useMatch' | 'useState'
325
+ > = {
326
+ useState: () => {
327
+ useRouterSubscription(router)
328
+ return router.state
329
+ },
181
330
  useRoute: (routeId) => {
182
331
  const route = router.getRoute(routeId)
183
332
  useRouterSubscription(router)
184
- if (!route) {
185
- throw new Error(
186
- `Could not find a route for route "${
187
- routeId as string
188
- }"! Did you forget to add it to your route config?`,
189
- )
190
- }
333
+ invariant(
334
+ route,
335
+ `Could not find a route for route "${
336
+ routeId as string
337
+ }"! Did you forget to add it to your route config?`,
338
+ )
191
339
  return route
192
340
  },
193
341
  useMatch: (routeId) => {
194
- if (routeId === rootRouteId) {
195
- throw new Error(
196
- `"${rootRouteId}" cannot be used with useMatch! Did you mean to useRoute("${rootRouteId}")?`,
197
- )
198
- }
342
+ useRouterSubscription(router)
343
+
344
+ invariant(
345
+ routeId !== rootRouteId,
346
+ `"${rootRouteId}" cannot be used with useMatch! Did you mean to useRoute("${rootRouteId}")?`,
347
+ )
348
+
199
349
  const runtimeMatch = useMatch()
200
350
  const match = router.state.matches.find((d) => d.routeId === routeId)
201
351
 
202
- if (!match) {
203
- throw new Error(
204
- `Could not find a match for route "${
205
- routeId as string
206
- }" being rendered in this component!`,
207
- )
208
- }
209
-
210
- if (runtimeMatch.routeId !== match?.routeId) {
211
- throw new Error(
212
- `useMatch('${
213
- match?.routeId as string
214
- }') is being called in a component that is meant to render the '${
215
- runtimeMatch.routeId
216
- }' route. Did you mean to 'useRoute(${
217
- match?.routeId as string
218
- })' instead?`,
219
- )
220
- }
352
+ invariant(
353
+ match,
354
+ `Could not find a match for route "${
355
+ routeId as string
356
+ }" being rendered in this component!`,
357
+ )
221
358
 
222
- useRouterSubscription(router)
359
+ invariant(
360
+ runtimeMatch.routeId == match?.routeId,
361
+ `useMatch('${
362
+ match?.routeId as string
363
+ }') is being called in a component that is meant to render the '${
364
+ runtimeMatch.routeId
365
+ }' route. Did you mean to 'useRoute(${
366
+ match?.routeId as string
367
+ })' instead?`,
368
+ )
223
369
 
224
370
  if (!match) {
225
- throw new Error('Match not found!')
371
+ invariant('Match not found!')
226
372
  }
227
373
 
228
374
  return match
229
375
  },
230
376
  }
231
377
 
232
- Object.assign(router, routerExt)
378
+ const routeExt = makeRouteExt(router.getRoute('/'), router)
379
+
380
+ Object.assign(router, routerExt, routeExt)
233
381
  },
234
382
  createRoute: ({ router, route }) => {
235
- const routeExt: Pick<AnyRoute, 'linkProps' | 'Link' | 'MatchRoute'> = {
236
- linkProps: (options) => {
237
- const {
238
- // custom props
239
- type,
240
- children,
241
- target,
242
- activeProps = () => ({ className: 'active' }),
243
- inactiveProps = () => ({}),
244
- activeOptions,
245
- disabled,
246
- // fromCurrent,
247
- hash,
248
- search,
249
- params,
250
- to,
251
- preload,
252
- preloadDelay,
253
- preloadMaxAge,
254
- replace,
255
- // element props
256
- style,
257
- className,
258
- onClick,
259
- onFocus,
260
- onMouseEnter,
261
- onMouseLeave,
262
- onTouchStart,
263
- onTouchEnd,
264
- ...rest
265
- } = options
266
-
267
- const linkInfo = route.buildLink(options)
268
-
269
- if (linkInfo.type === 'external') {
270
- const { href } = linkInfo
271
- return { href }
272
- }
273
-
274
- const {
275
- handleClick,
276
- handleFocus,
277
- handleEnter,
278
- handleLeave,
279
- isActive,
280
- next,
281
- } = linkInfo
282
-
283
- const composeHandlers =
284
- (handlers: (undefined | ((e: any) => void))[]) =>
285
- (e: React.SyntheticEvent) => {
286
- e.persist()
287
- handlers.forEach((handler) => {
288
- if (handler) handler(e)
289
- })
290
- }
291
-
292
- // Get the active props
293
- const resolvedActiveProps: React.HTMLAttributes<HTMLAnchorElement> =
294
- isActive ? functionalUpdate(activeProps) ?? {} : {}
295
-
296
- // Get the inactive props
297
- const resolvedInactiveProps: React.HTMLAttributes<HTMLAnchorElement> =
298
- isActive ? {} : functionalUpdate(inactiveProps) ?? {}
299
-
300
- return {
301
- ...resolvedActiveProps,
302
- ...resolvedInactiveProps,
303
- ...rest,
304
- href: disabled ? undefined : next.href,
305
- onClick: composeHandlers([handleClick, onClick]),
306
- onFocus: composeHandlers([handleFocus, onFocus]),
307
- onMouseEnter: composeHandlers([handleEnter, onMouseEnter]),
308
- onMouseLeave: composeHandlers([handleLeave, onMouseLeave]),
309
- target,
310
- style: {
311
- ...style,
312
- ...resolvedActiveProps.style,
313
- ...resolvedInactiveProps.style,
314
- },
315
- className:
316
- [
317
- className,
318
- resolvedActiveProps.className,
319
- resolvedInactiveProps.className,
320
- ]
321
- .filter(Boolean)
322
- .join(' ') || undefined,
323
- ...(disabled
324
- ? {
325
- role: 'link',
326
- 'aria-disabled': true,
327
- }
328
- : undefined),
329
- ['data-status']: isActive ? 'active' : undefined,
330
- }
331
- },
332
- Link: React.forwardRef((props: any, ref) => {
333
- const linkProps = route.linkProps(props)
334
-
335
- useRouterSubscription(router)
336
-
337
- return (
338
- <a
339
- {...{
340
- ref: ref as any,
341
- ...linkProps,
342
- children:
343
- typeof props.children === 'function'
344
- ? props.children({
345
- isActive:
346
- (linkProps as any)['data-status'] === 'active',
347
- })
348
- : props.children,
349
- }}
350
- />
351
- )
352
- }) as any,
353
- MatchRoute: (opts) => {
354
- const { pending, caseSensitive, children, ...rest } = opts
355
-
356
- const params = route.matchRoute(rest as any, {
357
- pending,
358
- caseSensitive,
359
- })
360
-
361
- // useRouterSubscription(router)
362
-
363
- if (!params) {
364
- return null
365
- }
366
-
367
- return typeof opts.children === 'function'
368
- ? opts.children(params as any)
369
- : (opts.children as any)
370
- },
371
- }
383
+ const routeExt = makeRouteExt(route, router)
372
384
 
373
385
  Object.assign(route, routeExt)
374
386
  },
@@ -392,14 +404,11 @@ export function RouterProvider<
392
404
  >({ children, router, ...rest }: RouterProps<TRouteConfig, TAllRouteInfo>) {
393
405
  router.update(rest)
394
406
 
395
- useSyncExternalStore(
396
- (cb) => router.subscribe(() => cb()),
397
- () => router.state,
398
- )
407
+ useRouterSubscription(router)
399
408
 
400
409
  useLayoutEffect(() => {
401
- router.mount()
402
- }, [])
410
+ return router.mount()
411
+ }, [router])
403
412
 
404
413
  return (
405
414
  <routerContext.Provider value={{ router }}>
@@ -410,7 +419,7 @@ export function RouterProvider<
410
419
  )
411
420
  }
412
421
 
413
- export function useRouter(): Router {
422
+ function useRouter(): Router {
414
423
  const value = React.useContext(routerContext)
415
424
  warning(!value, 'useRouter must be used inside a <Router> component!')
416
425
 
@@ -419,21 +428,21 @@ export function useRouter(): Router {
419
428
  return value.router as Router
420
429
  }
421
430
 
422
- export function useMatches(): RouteMatch[] {
431
+ function useMatches(): RouteMatch[] {
423
432
  return React.useContext(matchesContext)
424
433
  }
425
434
 
426
- export function useParentMatches(): RouteMatch[] {
427
- const router = useRouter()
428
- const match = useMatch()
429
- const matches = router.state.matches
430
- return matches.slice(
431
- 0,
432
- matches.findIndex((d) => d.matchId === match.matchId) - 1,
433
- )
434
- }
435
-
436
- export function useMatch<T>(): RouteMatch {
435
+ // function useParentMatches(): RouteMatch[] {
436
+ // const router = useRouter()
437
+ // const match = useMatch()
438
+ // const matches = router.state.matches
439
+ // return matches.slice(
440
+ // 0,
441
+ // matches.findIndex((d) => d.matchId === match.matchId) - 1,
442
+ // )
443
+ // }
444
+
445
+ function useMatch<T>(): RouteMatch {
437
446
  return useMatches()?.[0] as RouteMatch
438
447
  }
439
448
 
@@ -445,7 +454,7 @@ export function Outlet() {
445
454
 
446
455
  if (!childMatch) return null
447
456
 
448
- const element = (((): React.ReactNode => {
457
+ const element = ((): React.ReactNode => {
449
458
  if (!childMatch) {
450
459
  return null
451
460
  }
@@ -465,7 +474,7 @@ export function Outlet() {
465
474
  throw childMatch.error
466
475
  }
467
476
 
468
- return <DefaultCatchBoundary error={childMatch.error} />
477
+ return <DefaultErrorBoundary error={childMatch.error} />
469
478
  }
470
479
 
471
480
  if (childMatch.status === 'loading' || childMatch.status === 'idle') {
@@ -482,7 +491,7 @@ export function Outlet() {
482
491
  }
483
492
 
484
493
  return (childMatch.__.element as any) ?? router.options.defaultElement
485
- })() as JSX.Element) ?? <Outlet />
494
+ })() as JSX.Element
486
495
 
487
496
  const catchElement =
488
497
  childMatch?.options.catchElement ?? router.options.defaultCatchElement
@@ -516,7 +525,7 @@ class CatchBoundary extends React.Component<{
516
525
  })
517
526
  }
518
527
  render() {
519
- const catchElement = this.props.catchElement ?? DefaultCatchBoundary
528
+ const catchElement = this.props.catchElement ?? DefaultErrorBoundary
520
529
 
521
530
  if (this.state.error) {
522
531
  return typeof catchElement === 'function'
@@ -528,7 +537,7 @@ class CatchBoundary extends React.Component<{
528
537
  }
529
538
  }
530
539
 
531
- export function DefaultCatchBoundary({ error }: { error: any }) {
540
+ export function DefaultErrorBoundary({ error }: { error: any }) {
532
541
  return (
533
542
  <div style={{ padding: '.5rem', maxWidth: '100%' }}>
534
543
  <strong style={{ fontSize: '1.2rem' }}>Something went wrong!</strong>
@@ -566,3 +575,27 @@ export function DefaultCatchBoundary({ error }: { error: any }) {
566
575
  </div>
567
576
  )
568
577
  }
578
+
579
+ export function usePrompt(message: string, when: boolean | any): void {
580
+ const router = useRouter()
581
+
582
+ React.useEffect(() => {
583
+ if (!when) return
584
+
585
+ let unblock = router.history.block((transition) => {
586
+ if (window.confirm(message)) {
587
+ unblock()
588
+ transition.retry()
589
+ } else {
590
+ router.location.pathname = window.location.pathname
591
+ }
592
+ })
593
+
594
+ return unblock
595
+ }, [when, location, message])
596
+ }
597
+
598
+ export function Prompt({ message, when, children }: PromptProps) {
599
+ usePrompt(message, when ?? true)
600
+ return (children ?? null) as React.ReactNode
601
+ }