@tanstack/react-router 0.0.1-beta.4 → 0.0.1-beta.45

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/src/index.tsx CHANGED
@@ -1,16 +1,19 @@
1
1
  import * as React from 'react'
2
2
 
3
3
  import { useSyncExternalStore } from 'use-sync-external-store/shim'
4
+ // @ts-ignore
5
+ // import { useSyncExternalStore } from './uSES/useSyncExternalStoreShim'
6
+ import { createEffect, createRoot, untrack, unwrap } from '@solidjs/reactivity'
7
+ import { createStore } from '@solidjs/reactivity'
4
8
 
5
9
  import {
6
- AnyRoute,
7
- CheckId,
8
- rootRouteId,
9
- Router,
10
- RouterState,
11
- ToIdOption,
12
- } from '@tanstack/router-core'
13
- import {
10
+ Route,
11
+ RegisteredAllRouteInfo,
12
+ RegisteredRouter,
13
+ RouterStore,
14
+ last,
15
+ sharedClone,
16
+ Action,
14
17
  warning,
15
18
  RouterOptions,
16
19
  RouteMatch,
@@ -21,9 +24,7 @@ import {
21
24
  DefaultAllRouteInfo,
22
25
  functionalUpdate,
23
26
  createRouter,
24
- AnyRouteInfo,
25
27
  AllRouteInfo,
26
- RouteInfo,
27
28
  ValidFromPath,
28
29
  LinkOptions,
29
30
  RouteInfoByPath,
@@ -31,111 +32,43 @@ import {
31
32
  NoInfer,
32
33
  ToOptions,
33
34
  invariant,
35
+ Router,
36
+ Expand,
34
37
  } from '@tanstack/router-core'
35
38
 
36
39
  export * from '@tanstack/router-core'
37
40
 
38
- declare module '@tanstack/router-core' {
39
- interface FrameworkGenerics {
40
- Element: React.ReactNode
41
- // Any is required here so import() will work without having to do import().then(d => d.default)
42
- SyncOrAsyncElement: React.ReactNode | (() => Promise<any>)
43
- }
41
+ export * from '@solidjs/reactivity'
44
42
 
45
- interface Router<
46
- TRouteConfig extends AnyRouteConfig = RouteConfig,
47
- TAllRouteInfo extends AnyAllRouteInfo = AllRouteInfo<TRouteConfig>,
48
- > {
49
- useState: () => RouterState
50
- useRoute: <TId extends keyof TAllRouteInfo['routeInfoById']>(
51
- routeId: TId,
52
- ) => Route<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TId]>
53
- useMatch: <TId extends keyof TAllRouteInfo['routeInfoById']>(
54
- routeId: TId,
55
- ) => RouteMatch<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TId]>
56
- linkProps: <TTo extends string = '.'>(
57
- props: LinkPropsOptions<TAllRouteInfo, '/', TTo> &
58
- React.AnchorHTMLAttributes<HTMLAnchorElement>,
59
- ) => React.AnchorHTMLAttributes<HTMLAnchorElement>
60
- Link: <TTo extends string = '.'>(
61
- props: LinkPropsOptions<TAllRouteInfo, '/', TTo> &
62
- React.AnchorHTMLAttributes<HTMLAnchorElement> &
63
- Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'children'> & {
64
- // 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
65
- children?:
66
- | React.ReactNode
67
- | ((state: { isActive: boolean }) => React.ReactNode)
68
- },
69
- ) => JSX.Element
70
- MatchRoute: <TTo extends string = '.'>(
71
- props: ToOptions<TAllRouteInfo, '/', TTo> &
72
- MatchRouteOptions & {
73
- // 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
74
- children?:
75
- | React.ReactNode
76
- | ((
77
- params: RouteInfoByPath<
78
- TAllRouteInfo,
79
- ResolveRelativePath<'/', NoInfer<TTo>>
80
- >['allParams'],
81
- ) => React.ReactNode)
82
- },
83
- ) => JSX.Element
84
- }
43
+ type ReactNode = any
44
+
45
+ export type SyncRouteComponent<TProps = {}> = (props: TProps) => ReactNode
46
+
47
+ export type RouteComponent<TProps = {}> = SyncRouteComponent<TProps> & {
48
+ preload?: () => Promise<void>
49
+ }
50
+
51
+ export function lazy(
52
+ importer: () => Promise<{ default: SyncRouteComponent }>,
53
+ ): RouteComponent {
54
+ const lazyComp = React.lazy(importer as any)
55
+ let preloaded: Promise<SyncRouteComponent>
56
+
57
+ const finalComp = lazyComp as unknown as RouteComponent
85
58
 
86
- interface Route<
87
- TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
88
- TRouteInfo extends AnyRouteInfo = RouteInfo,
89
- > {
90
- useRoute: <
91
- TTo extends string = '.',
92
- TResolved extends string = ResolveRelativePath<
93
- TRouteInfo['id'],
94
- NoInfer<TTo>
95
- >,
96
- >(
97
- routeId: CheckId<
98
- TAllRouteInfo,
99
- TResolved,
100
- ToIdOption<TAllRouteInfo, TRouteInfo['id'], TTo>
101
- >,
102
- ) => Route<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TResolved]>
103
- linkProps: <TTo extends string = '.'>(
104
- props: LinkPropsOptions<TAllRouteInfo, TRouteInfo['fullPath'], TTo> &
105
- React.AnchorHTMLAttributes<HTMLAnchorElement>,
106
- ) => React.AnchorHTMLAttributes<HTMLAnchorElement>
107
- Link: <TTo extends string = '.'>(
108
- props: LinkPropsOptions<TAllRouteInfo, TRouteInfo['fullPath'], TTo> &
109
- React.AnchorHTMLAttributes<HTMLAnchorElement> &
110
- Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'children'> & {
111
- // 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
112
- children?:
113
- | React.ReactNode
114
- | ((state: { isActive: boolean }) => React.ReactNode)
115
- },
116
- ) => JSX.Element
117
- MatchRoute: <TTo extends string = '.'>(
118
- props: ToOptions<TAllRouteInfo, TRouteInfo['fullPath'], TTo> &
119
- MatchRouteOptions & {
120
- // 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
121
- children?:
122
- | React.ReactNode
123
- | ((
124
- params: RouteInfoByPath<
125
- TAllRouteInfo,
126
- ResolveRelativePath<TRouteInfo['fullPath'], NoInfer<TTo>>
127
- >['allParams'],
128
- ) => React.ReactNode)
129
- },
130
- ) => JSX.Element
59
+ finalComp.preload = async () => {
60
+ if (!preloaded) {
61
+ await importer()
62
+ }
131
63
  }
64
+
65
+ return finalComp
132
66
  }
133
67
 
134
- type LinkPropsOptions<
135
- TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
136
- TFrom extends ValidFromPath<TAllRouteInfo> = '/',
68
+ export type LinkPropsOptions<
69
+ TFrom extends RegisteredAllRouteInfo['routePaths'] = '/',
137
70
  TTo extends string = '.',
138
- > = LinkOptions<TAllRouteInfo, TFrom, TTo> & {
71
+ > = LinkOptions<RegisteredAllRouteInfo, TFrom, TTo> & {
139
72
  // A function that returns additional props for the `active` state of this link. These props override other props passed to the link (`style`'s are merged, `className`'s are concatenated)
140
73
  activeProps?:
141
74
  | React.AnchorHTMLAttributes<HTMLAnchorElement>
@@ -146,268 +79,297 @@ type LinkPropsOptions<
146
79
  | (() => React.AnchorHTMLAttributes<HTMLAnchorElement>)
147
80
  }
148
81
 
82
+ export type MakeUseMatchRouteOptions<
83
+ TFrom extends RegisteredAllRouteInfo['routePaths'] = '/',
84
+ TTo extends string = '.',
85
+ > = ToOptions<RegisteredAllRouteInfo, TFrom, TTo> & MatchRouteOptions
86
+
87
+ export type MakeMatchRouteOptions<
88
+ TFrom extends RegisteredAllRouteInfo['routePaths'] = '/',
89
+ TTo extends string = '.',
90
+ > = ToOptions<RegisteredAllRouteInfo, TFrom, TTo> &
91
+ MatchRouteOptions & {
92
+ // 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
93
+ children?:
94
+ | ReactNode
95
+ | ((
96
+ params: RouteInfoByPath<
97
+ RegisteredAllRouteInfo,
98
+ ResolveRelativePath<TFrom, NoInfer<TTo>>
99
+ >['allParams'],
100
+ ) => ReactNode)
101
+ }
102
+
103
+ export type MakeLinkPropsOptions<
104
+ TFrom extends ValidFromPath<RegisteredAllRouteInfo> = '/',
105
+ TTo extends string = '.',
106
+ > = LinkPropsOptions<TFrom, TTo> & React.AnchorHTMLAttributes<HTMLAnchorElement>
107
+
108
+ export type MakeLinkOptions<
109
+ TFrom extends RegisteredAllRouteInfo['routePaths'] = '/',
110
+ TTo extends string = '.',
111
+ > = LinkPropsOptions<TFrom, TTo> &
112
+ React.AnchorHTMLAttributes<HTMLAnchorElement> &
113
+ Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'children'> & {
114
+ // 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
115
+ children?: ReactNode | ((state: { isActive: boolean }) => ReactNode)
116
+ }
117
+
118
+ declare module '@tanstack/router-core' {
119
+ interface FrameworkGenerics {
120
+ Component: RouteComponent
121
+ ErrorComponent: RouteComponent<{
122
+ error: unknown
123
+ info: { componentStack: string }
124
+ }>
125
+ }
126
+
127
+ interface RouterOptions<TRouteConfig, TRouterContext> {
128
+ // ssrFooter?: () => JSX.Element | Node
129
+ }
130
+ }
131
+
149
132
  export type PromptProps = {
150
133
  message: string
151
134
  when?: boolean | any
152
- children?: React.ReactNode
135
+ children?: ReactNode
153
136
  }
154
137
 
155
138
  //
156
139
 
157
- const matchesContext = React.createContext<RouteMatch[]>(null!)
158
- const routerContext = React.createContext<{ router: Router<any, any> }>(null!)
140
+ export function useLinkProps<
141
+ TFrom extends ValidFromPath<RegisteredAllRouteInfo> = '/',
142
+ TTo extends string = '.',
143
+ >(
144
+ options: MakeLinkPropsOptions<TFrom, TTo>,
145
+ ): React.AnchorHTMLAttributes<HTMLAnchorElement> {
146
+ const router = useRouter()
159
147
 
160
- // Detect if we're in the DOM
161
- const isDOM = Boolean(
162
- typeof window !== 'undefined' &&
163
- window.document &&
164
- window.document.createElement,
165
- )
148
+ const {
149
+ // custom props
150
+ type,
151
+ children,
152
+ target,
153
+ activeProps = () => ({ className: 'active' }),
154
+ inactiveProps = () => ({}),
155
+ activeOptions,
156
+ disabled,
157
+ // fromCurrent,
158
+ hash,
159
+ search,
160
+ params,
161
+ to,
162
+ preload,
163
+ preloadDelay,
164
+ preloadMaxAge,
165
+ replace,
166
+ // element props
167
+ style,
168
+ className,
169
+ onClick,
170
+ onFocus,
171
+ onMouseEnter,
172
+ onMouseLeave,
173
+ onTouchStart,
174
+ onTouchEnd,
175
+ ...rest
176
+ } = options
177
+
178
+ const linkInfo = router.buildLink(options as any)
179
+
180
+ if (linkInfo.type === 'external') {
181
+ const { href } = linkInfo
182
+ return { href }
183
+ }
166
184
 
167
- const useLayoutEffect = isDOM ? React.useLayoutEffect : React.useEffect
185
+ const { handleClick, handleFocus, handleEnter, handleLeave, isActive, next } =
186
+ linkInfo
187
+
188
+ const reactHandleClick = (e: Event) => {
189
+ if (React.startTransition) {
190
+ // This is a hack for react < 18
191
+ React.startTransition(() => {
192
+ handleClick(e)
193
+ })
194
+ } else {
195
+ handleClick(e)
196
+ }
197
+ }
168
198
 
169
- export type MatchesProviderProps = {
170
- value: RouteMatch[]
171
- children: React.ReactNode
199
+ const composeHandlers =
200
+ (handlers: (undefined | ((e: any) => void))[]) =>
201
+ (e: React.SyntheticEvent) => {
202
+ if (e.persist) e.persist()
203
+ handlers.filter(Boolean).forEach((handler) => {
204
+ if (e.defaultPrevented) return
205
+ handler!(e)
206
+ })
207
+ }
208
+
209
+ // Get the active props
210
+ const resolvedActiveProps: React.HTMLAttributes<HTMLAnchorElement> = isActive
211
+ ? functionalUpdate(activeProps, {}) ?? {}
212
+ : {}
213
+
214
+ // Get the inactive props
215
+ const resolvedInactiveProps: React.HTMLAttributes<HTMLAnchorElement> =
216
+ isActive ? {} : functionalUpdate(inactiveProps, {}) ?? {}
217
+
218
+ return {
219
+ ...resolvedActiveProps,
220
+ ...resolvedInactiveProps,
221
+ ...rest,
222
+ href: disabled ? undefined : next.href,
223
+ onClick: composeHandlers([onClick, reactHandleClick]),
224
+ onFocus: composeHandlers([onFocus, handleFocus]),
225
+ onMouseEnter: composeHandlers([onMouseEnter, handleEnter]),
226
+ onMouseLeave: composeHandlers([onMouseLeave, handleLeave]),
227
+ target,
228
+ style: {
229
+ ...style,
230
+ ...resolvedActiveProps.style,
231
+ ...resolvedInactiveProps.style,
232
+ },
233
+ className:
234
+ [
235
+ className,
236
+ resolvedActiveProps.className,
237
+ resolvedInactiveProps.className,
238
+ ]
239
+ .filter(Boolean)
240
+ .join(' ') || undefined,
241
+ ...(disabled
242
+ ? {
243
+ role: 'link',
244
+ 'aria-disabled': true,
245
+ }
246
+ : undefined),
247
+ ['data-status']: isActive ? 'active' : undefined,
248
+ }
172
249
  }
173
250
 
174
- export function MatchesProvider(props: MatchesProviderProps) {
175
- return <matchesContext.Provider {...props} />
251
+ export interface LinkFn<
252
+ TDefaultFrom extends RegisteredAllRouteInfo['routePaths'] = '/',
253
+ TDefaultTo extends string = '.',
254
+ > {
255
+ <
256
+ TFrom extends RegisteredAllRouteInfo['routePaths'] = TDefaultFrom,
257
+ TTo extends string = TDefaultTo,
258
+ >(
259
+ props: MakeLinkOptions<TFrom, TTo>,
260
+ ): ReactNode
176
261
  }
177
262
 
178
- const useRouterSubscription = (router: Router<any, any>) => {
179
- useSyncExternalStore(
180
- (cb) => router.subscribe(() => cb()),
181
- () => router.state,
182
- () => router.state,
263
+ export const Link: LinkFn = React.forwardRef((props: any, ref) => {
264
+ const linkProps = useLinkProps(props)
265
+
266
+ return (
267
+ <a
268
+ {...{
269
+ ref: ref as any,
270
+ ...linkProps,
271
+ children:
272
+ typeof props.children === 'function'
273
+ ? props.children({
274
+ isActive: (linkProps as any)['data-status'] === 'active',
275
+ })
276
+ : props.children,
277
+ }}
278
+ />
183
279
  )
184
- }
280
+ }) as any
185
281
 
186
- export function createReactRouter<
187
- TRouteConfig extends AnyRouteConfig = RouteConfig,
188
- >(opts: RouterOptions<TRouteConfig>): Router<TRouteConfig> {
189
- const makeRouteExt = (
190
- route: AnyRoute,
191
- router: Router<any, any>,
192
- ): Pick<AnyRoute, 'useRoute' | 'linkProps' | 'Link' | 'MatchRoute'> => {
193
- return {
194
- useRoute: (subRouteId = '.' as any) => {
195
- const resolvedRouteId = router.resolvePath(
196
- route.routeId,
197
- subRouteId as string,
198
- )
199
- const resolvedRoute = router.getRoute(resolvedRouteId)
200
- useRouterSubscription(router)
201
- invariant(
202
- resolvedRoute,
203
- `Could not find a route for route "${
204
- resolvedRouteId as string
205
- }"! Did you forget to add it to your route config?`,
206
- )
207
- return resolvedRoute
208
- },
209
- linkProps: (options) => {
210
- const {
211
- // custom props
212
- type,
213
- children,
214
- target,
215
- activeProps = () => ({ className: 'active' }),
216
- inactiveProps = () => ({}),
217
- activeOptions,
218
- disabled,
219
- // fromCurrent,
220
- hash,
221
- search,
222
- params,
223
- to,
224
- preload,
225
- preloadDelay,
226
- preloadMaxAge,
227
- replace,
228
- // element props
229
- style,
230
- className,
231
- onClick,
232
- onFocus,
233
- onMouseEnter,
234
- onMouseLeave,
235
- onTouchStart,
236
- onTouchEnd,
237
- ...rest
238
- } = options
239
-
240
- const linkInfo = route.buildLink(options)
241
-
242
- if (linkInfo.type === 'external') {
243
- const { href } = linkInfo
244
- return { href }
245
- }
282
+ type MatchesContextValue = RouteMatch[]
246
283
 
247
- const {
248
- handleClick,
249
- handleFocus,
250
- handleEnter,
251
- handleLeave,
252
- isActive,
253
- next,
254
- } = linkInfo
255
-
256
- const composeHandlers =
257
- (handlers: (undefined | ((e: any) => void))[]) =>
258
- (e: React.SyntheticEvent) => {
259
- e.persist()
260
- handlers.forEach((handler) => {
261
- if (handler) handler(e)
262
- })
263
- }
284
+ export const matchesContext = React.createContext<MatchesContextValue>(null!)
285
+ export const routerContext = React.createContext<{ router: RegisteredRouter }>(
286
+ null!,
287
+ )
264
288
 
265
- // Get the active props
266
- const resolvedActiveProps: React.HTMLAttributes<HTMLAnchorElement> =
267
- isActive ? functionalUpdate(activeProps, {}) ?? {} : {}
268
-
269
- // Get the inactive props
270
- const resolvedInactiveProps: React.HTMLAttributes<HTMLAnchorElement> =
271
- isActive ? {} : functionalUpdate(inactiveProps, {}) ?? {}
272
-
273
- return {
274
- ...resolvedActiveProps,
275
- ...resolvedInactiveProps,
276
- ...rest,
277
- href: disabled ? undefined : next.href,
278
- onClick: composeHandlers([handleClick, onClick]),
279
- onFocus: composeHandlers([handleFocus, onFocus]),
280
- onMouseEnter: composeHandlers([handleEnter, onMouseEnter]),
281
- onMouseLeave: composeHandlers([handleLeave, onMouseLeave]),
282
- target,
283
- style: {
284
- ...style,
285
- ...resolvedActiveProps.style,
286
- ...resolvedInactiveProps.style,
287
- },
288
- className:
289
- [
290
- className,
291
- resolvedActiveProps.className,
292
- resolvedInactiveProps.className,
293
- ]
294
- .filter(Boolean)
295
- .join(' ') || undefined,
296
- ...(disabled
297
- ? {
298
- role: 'link',
299
- 'aria-disabled': true,
300
- }
301
- : undefined),
302
- ['data-status']: isActive ? 'active' : undefined,
303
- }
304
- },
305
- Link: React.forwardRef((props: any, ref) => {
306
- const linkProps = route.linkProps(props)
307
-
308
- useRouterSubscription(router)
309
-
310
- return (
311
- <a
312
- {...{
313
- ref: ref as any,
314
- ...linkProps,
315
- children:
316
- typeof props.children === 'function'
317
- ? props.children({
318
- isActive: (linkProps as any)['data-status'] === 'active',
319
- })
320
- : props.children,
321
- }}
322
- />
323
- )
324
- }) as any,
325
- MatchRoute: (opts) => {
326
- const { pending, caseSensitive, children, ...rest } = opts
289
+ export type MatchesProviderProps = {
290
+ value: MatchesContextValue
291
+ children: ReactNode
292
+ }
327
293
 
328
- const params = route.matchRoute(rest as any, {
329
- pending,
330
- caseSensitive,
331
- })
294
+ const EMPTY = {}
332
295
 
333
- if (!params) {
334
- return null
335
- }
296
+ export const __useStoreValue = <TSeed, TReturn>(
297
+ seed: () => TSeed,
298
+ selector?: (seed: TSeed) => TReturn,
299
+ ): TReturn => {
300
+ const valueRef = React.useRef<TReturn>(EMPTY as any)
336
301
 
337
- return typeof opts.children === 'function'
338
- ? opts.children(params as any)
339
- : (opts.children as any)
340
- },
341
- }
302
+ // If there is no selector, track the seed
303
+ // If there is a selector, do not track the seed
304
+ const getValue = () =>
305
+ (!selector ? seed() : selector(untrack(() => seed()))) as TReturn
306
+
307
+ // If empty, initialize the value
308
+ if (valueRef.current === EMPTY) {
309
+ valueRef.current = sharedClone(undefined, getValue())
342
310
  }
343
311
 
344
- const coreRouter = createRouter<TRouteConfig>({
345
- ...opts,
346
- createRouter: (router) => {
347
- const routerExt: Pick<Router<any, any>, 'useMatch' | 'useState'> = {
348
- useState: () => {
349
- useRouterSubscription(router)
350
- return router.state
351
- },
352
- useMatch: (routeId) => {
353
- useRouterSubscription(router)
354
-
355
- invariant(
356
- routeId !== rootRouteId,
357
- `"${rootRouteId}" cannot be used with useMatch! Did you mean to useRoute("${rootRouteId}")?`,
358
- )
359
-
360
- const runtimeMatch = useMatch()
361
- const match = router.state.matches.find((d) => d.routeId === routeId)
362
-
363
- invariant(
364
- match,
365
- `Could not find a match for route "${
366
- routeId as string
367
- }" being rendered in this component!`,
368
- )
369
-
370
- invariant(
371
- runtimeMatch.routeId == match?.routeId,
372
- `useMatch('${
373
- match?.routeId as string
374
- }') is being called in a component that is meant to render the '${
375
- runtimeMatch.routeId
376
- }' route. Did you mean to 'useRoute(${
377
- match?.routeId as string
378
- })' instead?`,
379
- )
380
-
381
- if (!match) {
382
- invariant('Match not found!')
383
- }
312
+ // Snapshot should just return the current cached value
313
+ const getSnapshot = React.useCallback(() => valueRef.current, [])
314
+
315
+ const getStore = React.useCallback((cb: () => void) => {
316
+ // A root is necessary to track effects
317
+ return createRoot(() => {
318
+ createEffect(() => {
319
+ // Read and update the value
320
+ // getValue will handle which values are accessed and
321
+ // thus tracked.
322
+ // sharedClone will both recursively track the end result
323
+ // and ensure that the previous value is structurally shared
324
+ // into the new version.
325
+ valueRef.current = unwrap(
326
+ // Unwrap the value to get rid of any proxy structures
327
+ sharedClone(valueRef.current, getValue()),
328
+ )
329
+ cb()
330
+ })
331
+ })
332
+ }, [])
384
333
 
385
- return match
386
- },
387
- }
334
+ return useSyncExternalStore(getStore, getSnapshot, getSnapshot)
335
+ }
388
336
 
389
- const routeExt = makeRouteExt(router.getRoute('/'), router)
337
+ const [store, setStore] = createStore({ foo: 'foo', bar: { baz: 'baz' } })
390
338
 
391
- Object.assign(router, routerExt, routeExt)
392
- },
393
- createRoute: ({ router, route }) => {
394
- const routeExt = makeRouteExt(route, router)
339
+ createRoot(() => {
340
+ let prev: any
395
341
 
396
- Object.assign(route, routeExt)
397
- },
398
- createElement: async (element) => {
399
- if (typeof element === 'function') {
400
- const res = (await element()) as any
401
-
402
- // Support direct import() calls
403
- if (typeof res === 'object' && res.default) {
404
- return React.createElement(res.default)
405
- } else {
406
- return res
407
- }
342
+ createEffect(() => {
343
+ console.log('effect')
344
+ const next = sharedClone(prev, store)
345
+ console.log(next)
346
+ prev = untrack(() => next)
347
+ })
348
+ })
349
+
350
+ setStore((s) => {
351
+ s.foo = '1'
352
+ })
353
+
354
+ setStore((s) => {
355
+ s.bar.baz = '2'
356
+ })
357
+
358
+ export function createReactRouter<
359
+ TRouteConfig extends AnyRouteConfig = RouteConfig,
360
+ TAllRouteInfo extends AnyAllRouteInfo = AllRouteInfo<TRouteConfig>,
361
+ TRouterContext = unknown,
362
+ >(
363
+ opts: RouterOptions<TRouteConfig, TRouterContext>,
364
+ ): Router<TRouteConfig, TAllRouteInfo, TRouterContext> {
365
+ const coreRouter = createRouter<TRouteConfig>({
366
+ ...opts,
367
+ loadComponent: async (component) => {
368
+ if (component.preload) {
369
+ await component.preload()
408
370
  }
409
371
 
410
- return element
372
+ return component as any
411
373
  },
412
374
  })
413
375
 
@@ -417,125 +379,314 @@ export function createReactRouter<
417
379
  export type RouterProps<
418
380
  TRouteConfig extends AnyRouteConfig = RouteConfig,
419
381
  TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
420
- > = RouterOptions<TRouteConfig> & {
421
- router: Router<TRouteConfig, TAllRouteInfo>
422
- // Children will default to `<Outlet />` if not provided
423
- children?: React.ReactNode
382
+ TRouterContext = unknown,
383
+ > = RouterOptions<TRouteConfig, TRouterContext> & {
384
+ router: Router<TRouteConfig, TAllRouteInfo, TRouterContext>
424
385
  }
425
386
 
426
387
  export function RouterProvider<
427
388
  TRouteConfig extends AnyRouteConfig = RouteConfig,
428
389
  TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
429
- >({ children, router, ...rest }: RouterProps<TRouteConfig, TAllRouteInfo>) {
390
+ TRouterContext = unknown,
391
+ >({
392
+ router,
393
+ ...rest
394
+ }: RouterProps<TRouteConfig, TAllRouteInfo, TRouterContext>) {
430
395
  router.update(rest)
431
396
 
432
- useRouterSubscription(router)
397
+ const [, , currentMatches] = __useStoreValue(
398
+ () => router.store,
399
+ (s) => [s.status, s.pendingMatches, s.currentMatches],
400
+ )
401
+
402
+ React.useEffect(router.mount, [router])
433
403
 
434
- useLayoutEffect(() => {
435
- return router.mount()
436
- }, [router])
404
+ console.log('current', currentMatches)
437
405
 
438
406
  return (
439
- <routerContext.Provider value={{ router }}>
440
- <MatchesProvider value={router.state.matches}>
441
- {children ?? <Outlet />}
442
- </MatchesProvider>
443
- </routerContext.Provider>
407
+ <>
408
+ <routerContext.Provider value={{ router: router as any }}>
409
+ <matchesContext.Provider value={[undefined!, ...currentMatches]}>
410
+ <Outlet />
411
+ </matchesContext.Provider>
412
+ </routerContext.Provider>
413
+ </>
444
414
  )
445
415
  }
446
416
 
447
- function useRouter(): Router {
417
+ export function useRouter(): RegisteredRouter {
448
418
  const value = React.useContext(routerContext)
449
419
  warning(!value, 'useRouter must be used inside a <Router> component!')
420
+ return value.router
421
+ }
450
422
 
451
- useRouterSubscription(value.router)
452
-
453
- return value.router as Router
423
+ export function useRouterStore<T = RouterStore>(
424
+ selector?: (state: Router['store']) => T,
425
+ ): T {
426
+ const router = useRouter()
427
+ return __useStoreValue(() => router.store, selector)
454
428
  }
455
429
 
456
- function useMatches(): RouteMatch[] {
430
+ export function useMatches(): RouteMatch[] {
457
431
  return React.useContext(matchesContext)
458
432
  }
459
433
 
460
- // function useParentMatches(): RouteMatch[] {
461
- // const router = useRouter()
462
- // const match = useMatch()
463
- // const matches = router.state.matches
464
- // return matches.slice(
465
- // 0,
466
- // matches.findIndex((d) => d.matchId === match.matchId) - 1,
467
- // )
468
- // }
434
+ export function useMatch<
435
+ TFrom extends keyof RegisteredAllRouteInfo['routeInfoById'],
436
+ TStrict extends boolean = true,
437
+ TRouteMatch = RouteMatch<
438
+ RegisteredAllRouteInfo,
439
+ RegisteredAllRouteInfo['routeInfoById'][TFrom]
440
+ >,
441
+ // TSelected = TRouteMatch,
442
+ >(opts?: {
443
+ from: TFrom
444
+ strict?: TStrict
445
+ // select?: (match: TRouteMatch) => TSelected
446
+ }): TStrict extends true ? TRouteMatch : TRouteMatch | undefined {
447
+ const router = useRouter()
448
+ const nearestMatch = useMatches()[0]!
449
+ const match = opts?.from
450
+ ? router.store.currentMatches.find((d) => d.routeId === opts?.from)
451
+ : nearestMatch
452
+
453
+ invariant(
454
+ match,
455
+ `Could not find ${
456
+ opts?.from ? `an active match from "${opts.from}"` : 'a nearest match!'
457
+ }`,
458
+ )
459
+
460
+ if (opts?.strict ?? true) {
461
+ invariant(
462
+ nearestMatch.routeId == match?.routeId,
463
+ `useMatch("${
464
+ match?.routeId as string
465
+ }") is being called in a component that is meant to render the '${
466
+ nearestMatch.routeId
467
+ }' route. Did you mean to 'useMatch("${
468
+ match?.routeId as string
469
+ }", { strict: false })' or 'useRoute("${
470
+ match?.routeId as string
471
+ }")' instead?`,
472
+ )
473
+ }
474
+
475
+ __useStoreValue(() => match!.store)
469
476
 
470
- function useMatch<T>(): RouteMatch {
471
- return useMatches()?.[0] as RouteMatch
477
+ return match as any
472
478
  }
473
479
 
474
- export function Outlet() {
480
+ export function useRoute<
481
+ TId extends keyof RegisteredAllRouteInfo['routeInfoById'] = '/',
482
+ >(
483
+ routeId: TId,
484
+ ): Route<RegisteredAllRouteInfo, RegisteredAllRouteInfo['routeInfoById'][TId]> {
475
485
  const router = useRouter()
476
- const [, ...matches] = useMatches()
486
+ const resolvedRoute = router.getRoute(routeId as any)
477
487
 
478
- const childMatch = matches[0]
488
+ invariant(
489
+ resolvedRoute,
490
+ `Could not find a route for route "${
491
+ routeId as string
492
+ }"! Did you forget to add it to your route config?`,
493
+ )
479
494
 
480
- if (!childMatch) return null
495
+ return resolvedRoute as any
496
+ }
481
497
 
482
- const element = ((): React.ReactNode => {
483
- if (!childMatch) {
484
- return null
485
- }
498
+ export function useLoaderData<
499
+ TFrom extends keyof RegisteredAllRouteInfo['routeInfoById'] = '/',
500
+ TStrict extends boolean = true,
501
+ TLoaderData = RegisteredAllRouteInfo['routeInfoById'][TFrom]['loaderData'],
502
+ TSelected = TLoaderData,
503
+ >(opts?: {
504
+ from: TFrom
505
+ strict?: TStrict
506
+ select?: (loaderData: TLoaderData) => TSelected
507
+ }): TStrict extends true ? TSelected : TSelected | undefined {
508
+ const match = useMatch(opts) as any
509
+ return __useStoreValue(() => match?.store.loaderData, opts?.select)
510
+ }
486
511
 
487
- const errorElement =
488
- childMatch.__.errorElement ?? router.options.defaultErrorElement
512
+ export function useSearch<
513
+ TFrom extends keyof RegisteredAllRouteInfo['routeInfoById'],
514
+ TStrict extends boolean = true,
515
+ TSearch = RegisteredAllRouteInfo['routeInfoById'][TFrom]['fullSearchSchema'],
516
+ TSelected = TSearch,
517
+ >(opts?: {
518
+ from: TFrom
519
+ strict?: TStrict
520
+ select?: (search: TSearch) => TSelected
521
+ }): TStrict extends true ? TSelected : TSelected | undefined {
522
+ const match = useMatch(opts)
523
+ return __useStoreValue(() => match?.store.search, opts?.select) as any
524
+ }
489
525
 
490
- if (childMatch.status === 'error') {
491
- if (errorElement) {
492
- return errorElement as any
493
- }
526
+ export function useParams<
527
+ TFrom extends keyof RegisteredAllRouteInfo['routeInfoById'] = '/',
528
+ TDefaultSelected = Expand<
529
+ RegisteredAllRouteInfo['allParams'] &
530
+ RegisteredAllRouteInfo['routeInfoById'][TFrom]['allParams']
531
+ >,
532
+ TSelected = TDefaultSelected,
533
+ >(opts?: {
534
+ from: TFrom
535
+ select?: (search: TDefaultSelected) => TSelected
536
+ }): TSelected {
537
+ const router = useRouter()
538
+ return __useStoreValue(
539
+ () => last(router.store.currentMatches)?.params as any,
540
+ opts?.select,
541
+ )
542
+ }
494
543
 
495
- if (
496
- childMatch.options.useErrorBoundary ||
497
- router.options.useErrorBoundary
498
- ) {
499
- throw childMatch.error
500
- }
544
+ export function useNavigate<
545
+ TDefaultFrom extends keyof RegisteredAllRouteInfo['routeInfoById'] = '/',
546
+ >(defaultOpts: { from?: TDefaultFrom }) {
547
+ return <
548
+ TFrom extends keyof RegisteredAllRouteInfo['routeInfoById'] = TDefaultFrom,
549
+ TTo extends string = '.',
550
+ >(
551
+ opts: MakeLinkOptions<TFrom, TTo>,
552
+ ) => {
553
+ const router = useRouter()
554
+ return router.navigate({ ...defaultOpts, ...(opts as any) })
555
+ }
556
+ }
501
557
 
502
- return <DefaultErrorBoundary error={childMatch.error} />
503
- }
558
+ export function useAction<
559
+ TFrom extends keyof RegisteredAllRouteInfo['routeInfoById'] = '/',
560
+ TFromRoute extends RegisteredAllRouteInfo['routeInfoById'][TFrom] = RegisteredAllRouteInfo['routeInfoById'][TFrom],
561
+ >(opts: {
562
+ from: TFrom
563
+ }): Action<TFromRoute['actionPayload'], TFromRoute['actionResponse']> {
564
+ const route = useRoute(opts.from)
565
+ const action = route.action
566
+ __useStoreValue(() => action)
567
+ return action as any
568
+ }
504
569
 
505
- if (childMatch.status === 'loading' || childMatch.status === 'idle') {
506
- if (childMatch.isPending) {
507
- const pendingElement =
508
- childMatch.__.pendingElement ?? router.options.defaultPendingElement
570
+ export function useMatchRoute() {
571
+ const router = useRouter()
509
572
 
510
- if (childMatch.options.pendingMs || pendingElement) {
511
- return (pendingElement as any) ?? null
512
- }
513
- }
573
+ return <
574
+ TFrom extends ValidFromPath<RegisteredAllRouteInfo> = '/',
575
+ TTo extends string = '.',
576
+ >(
577
+ opts: MakeUseMatchRouteOptions<TFrom, TTo>,
578
+ ) => {
579
+ const { pending, caseSensitive, ...rest } = opts
580
+
581
+ return router.matchRoute(rest as any, {
582
+ pending,
583
+ caseSensitive,
584
+ })
585
+ }
586
+ }
587
+
588
+ export function MatchRoute<
589
+ TFrom extends ValidFromPath<RegisteredAllRouteInfo> = '/',
590
+ TTo extends string = '.',
591
+ >(props: MakeMatchRouteOptions<TFrom, TTo>): any {
592
+ const matchRoute = useMatchRoute()
593
+ const params = matchRoute(props)
514
594
 
515
- return null
595
+ if (!params) {
596
+ return null
597
+ }
598
+
599
+ return React.createElement(
600
+ typeof props.children === 'function'
601
+ ? (props.children as any)(params)
602
+ : props.children,
603
+ props as any,
604
+ )
605
+ }
606
+
607
+ export function Outlet() {
608
+ const router = useRouter()
609
+ const matches = useMatches().slice(1)
610
+ const match = matches[0]
611
+
612
+ const defaultPending = React.useCallback(() => null, [])
613
+
614
+ __useStoreValue(() => match?.store)
615
+
616
+ const Inner = React.useCallback((props: { match: RouteMatch }): any => {
617
+ if (props.match.store.status === 'error') {
618
+ throw props.match.store.error
516
619
  }
517
620
 
518
- return (childMatch.__.element as any) ?? router.options.defaultElement
519
- })() as JSX.Element
621
+ if (props.match.store.status === 'success') {
622
+ return React.createElement(
623
+ (props.match.__.component as any) ??
624
+ router.options.defaultComponent ??
625
+ Outlet,
626
+ )
627
+ }
520
628
 
521
- const catchElement =
522
- childMatch?.options.catchElement ?? router.options.defaultCatchElement
629
+ if (props.match.store.status === 'loading') {
630
+ throw props.match.__.loadPromise
631
+ }
632
+
633
+ invariant(
634
+ false,
635
+ 'Idle routeMatch status encountered during rendering! You should never see this. File an issue!',
636
+ )
637
+ }, [])
638
+
639
+ if (!match) {
640
+ return null
641
+ }
642
+
643
+ const PendingComponent = (match.__.pendingComponent ??
644
+ router.options.defaultPendingComponent ??
645
+ defaultPending) as any
646
+
647
+ const errorComponent =
648
+ match.__.errorComponent ?? router.options.defaultErrorComponent
523
649
 
524
650
  return (
525
- <MatchesProvider value={matches} key={childMatch.matchId}>
526
- <CatchBoundary catchElement={catchElement}>{element}</CatchBoundary>
527
- </MatchesProvider>
651
+ <matchesContext.Provider value={matches}>
652
+ <React.Suspense fallback={<PendingComponent />}>
653
+ <CatchBoundary
654
+ key={match.routeId}
655
+ errorComponent={errorComponent}
656
+ match={match as any}
657
+ >
658
+ <Inner match={match} />
659
+ </CatchBoundary>
660
+ </React.Suspense>
661
+ {/* Provide a suffix suspense boundary to make sure the router is
662
+ ready to be dehydrated on the server */}
663
+ {/* {router.options.ssrFooter && match.matchId === rootRouteId ? (
664
+ <React.Suspense fallback={null}>
665
+ {(() => {
666
+ if (router.store.pending) {
667
+ throw router.navigationPromise
668
+ }
669
+
670
+ return router.options.ssrFooter()
671
+ })()}
672
+ </React.Suspense>
673
+ ) : null} */}
674
+ </matchesContext.Provider>
528
675
  )
529
676
  }
530
677
 
531
678
  class CatchBoundary extends React.Component<{
532
679
  children: any
533
- catchElement: any
680
+ errorComponent: any
681
+ match: RouteMatch
534
682
  }> {
535
683
  state = {
536
684
  error: false,
685
+ info: undefined,
537
686
  }
687
+
538
688
  componentDidCatch(error: any, info: any) {
689
+ console.error(`Error in route match: ${this.props.match.matchId}`)
539
690
  console.error(error)
540
691
 
541
692
  this.setState({
@@ -543,23 +694,61 @@ class CatchBoundary extends React.Component<{
543
694
  info,
544
695
  })
545
696
  }
546
- reset = () => {
547
- this.setState({
548
- error: false,
549
- info: false,
550
- })
551
- }
697
+
552
698
  render() {
553
- const catchElement = this.props.catchElement ?? DefaultErrorBoundary
699
+ return (
700
+ <CatchBoundaryInner
701
+ {...this.props}
702
+ errorState={this.state}
703
+ reset={() => this.setState({})}
704
+ />
705
+ )
706
+ }
707
+ }
708
+
709
+ // This is the messiest thing ever... I'm either seriously tired (likely) or
710
+ // there has to be a better way to reset error boundaries when the
711
+ // router's location key changes.
712
+ function CatchBoundaryInner(props: {
713
+ children: any
714
+ errorComponent: any
715
+ errorState: { error: unknown; info: any }
716
+ reset: () => void
717
+ }) {
718
+ const [activeErrorState, setActiveErrorState] = React.useState(
719
+ props.errorState,
720
+ )
721
+ const router = useRouter()
722
+ const errorComponent = props.errorComponent ?? DefaultErrorBoundary
554
723
 
555
- if (this.state.error) {
556
- return typeof catchElement === 'function'
557
- ? catchElement(this.state)
558
- : catchElement
724
+ React.useEffect(() => {
725
+ if (activeErrorState) {
726
+ let prevKey = router.store.currentLocation.key
727
+ return createRoot(() =>
728
+ createEffect(() => {
729
+ if (router.store.currentLocation.key !== prevKey) {
730
+ prevKey = router.store.currentLocation.key
731
+ setActiveErrorState({} as any)
732
+ }
733
+ }),
734
+ )
559
735
  }
560
736
 
561
- return this.props.children
737
+ return
738
+ }, [activeErrorState])
739
+
740
+ React.useEffect(() => {
741
+ if (props.errorState.error) {
742
+ setActiveErrorState(props.errorState)
743
+ }
744
+ props.reset()
745
+ }, [props.errorState.error])
746
+
747
+ if (props.errorState.error) {
748
+ return React.createElement(errorComponent, activeErrorState)
562
749
  }
750
+
751
+ return props.children
563
752
  }
564
753
 
565
754
  export function DefaultErrorBoundary({ error }: { error: any }) {
@@ -584,19 +773,6 @@ export function DefaultErrorBoundary({ error }: { error: any }) {
584
773
  ) : null}
585
774
  </pre>
586
775
  </div>
587
- <div style={{ height: '1rem' }} />
588
- <div
589
- style={{
590
- fontSize: '.8em',
591
- borderLeft: '3px solid rgba(127, 127, 127, 1)',
592
- paddingLeft: '.5rem',
593
- opacity: 0.5,
594
- }}
595
- >
596
- If you are the owner of this website, it's highly recommended that you
597
- configure your own custom Catch/Error boundaries for the router. You can
598
- optionally configure a boundary for each route.
599
- </div>
600
776
  </div>
601
777
  )
602
778
  }
@@ -612,15 +788,32 @@ export function usePrompt(message: string, when: boolean | any): void {
612
788
  unblock()
613
789
  transition.retry()
614
790
  } else {
615
- router.location.pathname = window.location.pathname
791
+ router.store.currentLocation.pathname = window.location.pathname
616
792
  }
617
793
  })
618
794
 
619
795
  return unblock
620
- }, [when, location, message])
796
+ }, [when, message])
621
797
  }
622
798
 
623
799
  export function Prompt({ message, when, children }: PromptProps) {
624
800
  usePrompt(message, when ?? true)
625
- return (children ?? null) as React.ReactNode
801
+ return (children ?? null) as ReactNode
626
802
  }
803
+
804
+ // function circularStringify(obj: any) {
805
+ // const seen = new Set()
806
+
807
+ // return (
808
+ // JSON.stringify(obj, (_, value) => {
809
+ // if (typeof value === 'function') {
810
+ // return undefined
811
+ // }
812
+ // if (typeof value === 'object' && value !== null) {
813
+ // if (seen.has(value)) return
814
+ // seen.add(value)
815
+ // }
816
+ // return value
817
+ // }) || ''
818
+ // )
819
+ // }