-
-
Notifications
You must be signed in to change notification settings - Fork 881
/
Copy pathlazyRouteComponent.tsx
112 lines (100 loc) · 3.58 KB
/
lazyRouteComponent.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import * as React from 'react'
import type { AsyncRouteComponent } from './route'
// If the load fails due to module not found, it may mean a new version of
// the build was deployed and the user's browser is still using an old version.
// If this happens, the old version in the user's browser would have an outdated
// URL to the lazy module.
// In that case, we want to attempt one window refresh to get the latest.
function isModuleNotFoundError(error: any): boolean {
// chrome: "Failed to fetch dynamically imported module: http://localhost:5173/src/routes/posts.index.tsx?tsr-split"
// firefox: "error loading dynamically imported module: http://localhost:5173/src/routes/posts.index.tsx?tsr-split"
// safari: "Importing a module script failed."
if (typeof error?.message !== 'string') return false
return (
error.message.startsWith('Failed to fetch dynamically imported module') ||
error.message.startsWith('error loading dynamically imported module') ||
error.message.startsWith('Importing a module script failed')
)
}
export function ClientOnly({
children,
fallback = null,
}: React.PropsWithChildren<{ fallback?: React.ReactNode }>) {
return useHydrated() ? <>{children}</> : <>{fallback}</>
}
function subscribe() {
return () => {}
}
export function useHydrated() {
return React.useSyncExternalStore(
subscribe,
() => true,
() => false,
)
}
export function lazyRouteComponent<
T extends Record<string, any>,
TKey extends keyof T = 'default',
>(
importer: () => Promise<T>,
exportName?: TKey,
): T[TKey] extends (props: infer TProps) => any
? AsyncRouteComponent<TProps>
: never {
let loadPromise: Promise<any> | undefined
let comp: T[TKey] | T['default']
let error: any
let reload: boolean
const load = () => {
if (!loadPromise) {
loadPromise = importer()
.then((res) => {
loadPromise = undefined
comp = res[exportName ?? 'default']
})
.catch((err) => {
// We don't want an error thrown from preload in this case, because
// there's nothing we want to do about module not found during preload.
// Record the error, the rest is handled during the render path.
error = err
if (isModuleNotFoundError(error)) {
if (
error instanceof Error &&
typeof window !== 'undefined' &&
typeof sessionStorage !== 'undefined'
) {
// Again, we want to reload one time on module not found error and not enter
// a reload loop if there is some other issue besides an old deploy.
// That's why we store our reload attempt in sessionStorage.
// Use error.message as key because it contains the module path that failed.
const storageKey = `tanstack_router_reload:${error.message}`
if (!sessionStorage.getItem(storageKey)) {
sessionStorage.setItem(storageKey, '1')
reload = true
}
}
}
})
}
return loadPromise
}
const lazyComp = function Lazy(props: any) {
// Now that we're out of preload and into actual render path,
if (reload) {
// If it was a module loading error,
// throw eternal suspense while we wait for window to reload
window.location.reload()
throw new Promise(() => {})
}
if (error) {
// Otherwise, just throw the error
throw error
}
if (!comp) {
throw load()
}
return React.createElement(comp, props)
}
;(lazyComp as any).preload = load
return lazyComp as any
}