UNPKG

5.86 kBPlain TextView Raw
1/* eslint-disable react-hooks/rules-of-hooks */
2import { useEffect, useRef } from 'react'
3import { getURL } from './getURL'
4import { useActiveParams, useParams } from './hooks'
5import { resolveHref } from './link/href'
6import { useRouteNode } from './Route'
7import { CACHE_KEY } from './router/constants'
8import { preloadingLoader } from './router/router'
9import type { LoaderProps } from './types'
10import { dynamicImport } from './utils/dynamicImport'
11import { weakKey } from './utils/weakKey'
12import { getLoaderPath } from './cleanUrl'
13
14const promises: Record<string, undefined | Promise<void>> = {}
15const errors = {}
16const loadedData: Record<string, any> = {}
17
18export function useLoader<
19 Loader extends Function,
20 Returned = Loader extends (p: any) => any ? ReturnType<Loader> : unknown,
21>(loader: Loader): Returned extends Promise<any> ? Awaited<Returned> : Returned {
22 const preloadedProps = globalThis['__vxrnLoaderProps__'] as LoaderProps | undefined
23
24 // server side we just run the loader directly
25 if (typeof window === 'undefined') {
26 return useAsyncFn(
27 loader,
28 preloadedProps || {
29 params: useActiveParams(),
30 }
31 )
32 }
33
34 const isNative = process.env.TAMAGUI_TARGET === 'native'
35 const routeNode = useRouteNode()
36 const params = useParams()
37
38 // Cannot use usePathname() here since it will change every time the route changes,
39 // but here here we want to get the current local pathname which renders this screen.
40 const pathName =
41 '/' + resolveHref({ pathname: routeNode?.route || '', params }).replace(/index$/, '')
42
43 const currentPath =
44 (isNative ? null : globalThis['__vxrntodopath']) || // @zetavg: not sure why we're using `globalThis['__vxrntodopath']` here, but this breaks native when switching between tabs where the value stays with the previous path, so ignoring this on native
45 // TODO likely either not needed or needs proper path from server side
46 (typeof window !== 'undefined' ? window.location?.pathname || pathName : '/')
47
48 // only if it matches current route
49 const preloadedData =
50 preloadedProps?.path === currentPath ? globalThis['__vxrnLoaderData__'] : undefined
51
52 const currentData = useRef(preloadedData)
53
54 useEffect(() => {
55 if (preloadedData) {
56 loadedData[currentPath] = preloadedData
57 delete globalThis['__vxrnLoaderData__']
58 }
59 // eslint-disable-next-line react-hooks/exhaustive-deps
60 }, [preloadedData])
61
62 if (errors[currentPath]) {
63 throw errors[currentPath]
64 }
65
66 const loaded = loadedData[currentPath]
67 if (typeof loaded !== 'undefined') {
68 return loaded
69 }
70
71 if (!preloadedData) {
72 if (preloadingLoader[currentPath]) {
73 if (typeof preloadingLoader[currentPath] === 'function') {
74 preloadingLoader[currentPath] = preloadingLoader[currentPath]()
75 }
76 promises[currentPath] = preloadingLoader[currentPath]
77 .then((val) => {
78 loadedData[currentPath] = val
79 })
80 .catch((err) => {
81 console.error(`Error loading loader`, err)
82 errors[currentPath] = err
83 delete promises[currentPath]
84 delete preloadingLoader[currentPath]
85 })
86 }
87
88 if (!promises[currentPath]) {
89 const getData = async () => {
90 // for native add a prefix to route around vite dev server being in front of ours
91 const loaderJSUrl = getLoaderPath(currentPath, true)
92
93 try {
94 const response = await (async () => {
95 if (isNative) {
96 const nativeLoaderJSUrl = `${loaderJSUrl}&platform=ios` /* TODO: platform */
97
98 try {
99 // On native, we need to fetch the loader code and eval it
100 const loaderJsCodeResp = await fetch(nativeLoaderJSUrl)
101 if (!loaderJsCodeResp.ok) {
102 throw new Error(`Response not ok: ${loaderJsCodeResp.status}`)
103 }
104 const loaderJsCode = await loaderJsCodeResp.text()
105 // biome-ignore lint/security/noGlobalEval: we can't use dynamic `import` on native so we need to fetch and `eval` the code
106 const result = eval(
107 `() => { var exports = {}; ${loaderJsCode}; return exports; }`
108 )()
109
110 if (typeof result.loader !== 'function') {
111 throw new Error("Loader code isn't exporting a `loader` function")
112 }
113
114 return result
115 } catch (e) {
116 console.error(`Error fetching loader from URL: ${nativeLoaderJSUrl}, ${e}`)
117 return { loader: () => ({}) }
118 }
119 }
120
121 // On web, we can use import to dynamically load the loader
122 return await dynamicImport(loaderJSUrl)
123 })()
124
125 loadedData[currentPath] = response.loader()
126 return loadedData[currentPath]
127 } catch (err) {
128 console.error(`Error calling loader: ${err}`)
129 errors[currentPath] = err
130 delete promises[currentPath]
131 return null
132 }
133 }
134 promises[currentPath] = getData()
135 }
136
137 throw promises[currentPath]
138 }
139
140 return currentData.current
141}
142
143const results = new Map()
144const started = new Map()
145
146function useAsyncFn(val: any, props?: any) {
147 const key = (val ? weakKey(val) : '') + JSON.stringify(props)
148
149 if (val) {
150 if (!started.get(key)) {
151 started.set(key, true)
152
153 let next = val(props)
154 if (next instanceof Promise) {
155 next = next
156 .then((final) => {
157 results.set(key, final)
158 })
159 .catch((err) => {
160 console.error(`Error running loader()`, err)
161 results.set(key, undefined)
162 })
163 }
164 results.set(key, next)
165 }
166 }
167
168 const current = results.get(key)
169
170 if (current instanceof Promise) {
171 throw current
172 }
173
174 return current
175}