1 |
|
2 | import { useEffect, useRef } from 'react'
|
3 | import { getURL } from './getURL'
|
4 | import { useActiveParams, useParams } from './hooks'
|
5 | import { resolveHref } from './link/href'
|
6 | import { useRouteNode } from './Route'
|
7 | import { CACHE_KEY } from './router/constants'
|
8 | import { preloadingLoader } from './router/router'
|
9 | import type { LoaderProps } from './types'
|
10 | import { dynamicImport } from './utils/dynamicImport'
|
11 | import { weakKey } from './utils/weakKey'
|
12 | import { getLoaderPath } from './cleanUrl'
|
13 |
|
14 | const promises: Record<string, undefined | Promise<void>> = {}
|
15 | const errors = {}
|
16 | const loadedData: Record<string, any> = {}
|
17 |
|
18 | export 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 |
|
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 |
|
39 |
|
40 | const pathName =
|
41 | '/' + resolveHref({ pathname: routeNode?.route || '', params }).replace(/index$/, '')
|
42 |
|
43 | const currentPath =
|
44 | (isNative ? null : globalThis['__vxrntodopath']) ||
|
45 |
|
46 | (typeof window !== 'undefined' ? window.location?.pathname || pathName : '/')
|
47 |
|
48 |
|
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 |
|
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 |
|
91 | const loaderJSUrl = getLoaderPath(currentPath, true)
|
92 |
|
93 | try {
|
94 | const response = await (async () => {
|
95 | if (isNative) {
|
96 | const nativeLoaderJSUrl = `${loaderJSUrl}&platform=ios`
|
97 |
|
98 | try {
|
99 |
|
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 |
|
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 |
|
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 |
|
143 | const results = new Map()
|
144 | const started = new Map()
|
145 |
|
146 | function 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 | }
|