UNPKG

one

Version:

One is a new React Framework that makes Vite serve both native and web.

170 lines (141 loc) 5.36 kB
/* eslint-disable react-hooks/rules-of-hooks */ import { useEffect, useRef } from 'react' import { useActiveParams, useParams, usePathname } from './hooks' import { resolveHref } from './link/href' import { preloadingLoader } from './router/router' import { getLoaderPath } from './utils/cleanUrl' import { dynamicImport } from './utils/dynamicImport' import { weakKey } from './utils/weakKey' import { useServerContext } from './vite/one-server-only' const promises: Record<string, undefined | Promise<void>> = {} const errors = {} const loadedData: Record<string, any> = {} export function useLoader< Loader extends Function, Returned = Loader extends (p: any) => any ? ReturnType<Loader> : unknown, >(loader: Loader): Returned extends Promise<any> ? Awaited<Returned> : Returned { const { loaderProps: loaderPropsFromServerContext, loaderData: loaderDataFromServerContext } = useServerContext() || {} // server side we just run the loader directly if (typeof window === 'undefined') { return useAsyncFn( loader, loaderPropsFromServerContext || { path: usePathname(), params: useActiveParams(), } ) } const params = useParams() const pathname = usePathname() // Cannot use usePathname() here since it will change every time the route changes, // but here here we want to get the current local pathname which renders this screen. const currentPath = resolveHref({ pathname: pathname, params }) .replace(/index$/, '') .replace(/\?.*/, '') // only if it matches current route const preloadedData = loaderPropsFromServerContext?.path === currentPath ? loaderDataFromServerContext : undefined const currentData = useRef(preloadedData) useEffect(() => { if (preloadedData) { loadedData[currentPath] = preloadedData } // eslint-disable-next-line react-hooks/exhaustive-deps }, [preloadedData]) if (errors[currentPath]) { throw errors[currentPath] } const loaded = loadedData[currentPath] if (typeof loaded !== 'undefined') { return loaded } if (!preloadedData) { if (preloadingLoader[currentPath]) { if (typeof preloadingLoader[currentPath] === 'function') { preloadingLoader[currentPath] = preloadingLoader[currentPath]() } promises[currentPath] = preloadingLoader[currentPath] .then((val) => { loadedData[currentPath] = val }) .catch((err) => { console.error(`Error loading loader`, err) errors[currentPath] = err delete promises[currentPath] delete preloadingLoader[currentPath] }) } if (!promises[currentPath]) { const getData = async () => { // for native add a prefix to route around vite dev server being in front of ours const loaderJSUrl = getLoaderPath(currentPath, true) try { const response = await (async () => { if (process.env.TAMAGUI_TARGET === 'native') { const nativeLoaderJSUrl = `${loaderJSUrl}?platform=ios` /* TODO: platform */ try { // On native, we need to fetch the loader code and eval it const loaderJsCodeResp = await fetch(nativeLoaderJSUrl) if (!loaderJsCodeResp.ok) { throw new Error(`Response not ok: ${loaderJsCodeResp.status}`) } const loaderJsCode = await loaderJsCodeResp.text() // biome-ignore lint/security/noGlobalEval: we can't use dynamic `import` on native so we need to fetch and `eval` the code const result = eval( `() => { var exports = {}; ${loaderJsCode}; return exports; }` )() if (typeof result.loader !== 'function') { throw new Error("Loader code isn't exporting a `loader` function") } return result } catch (e) { console.error(`Error fetching loader from URL: ${nativeLoaderJSUrl}, ${e}`) return { loader: () => ({}) } } } // On web, we can use import to dynamically load the loader return await dynamicImport(loaderJSUrl) })() loadedData[currentPath] = response.loader() return loadedData[currentPath] } catch (err) { console.error(`Error calling loader: ${err}`) errors[currentPath] = err delete promises[currentPath] return null } } promises[currentPath] = getData() } throw promises[currentPath] } return currentData.current } const results = new Map() const started = new Map() function useAsyncFn(val: any, props?: any) { const key = (val ? weakKey(val) : '') + JSON.stringify(props) if (val) { if (!started.get(key)) { started.set(key, true) let next = val(props) if (next instanceof Promise) { next = next .then((final) => { results.set(key, final) }) .catch((err) => { console.error(`Error running loader()`, err) results.set(key, undefined) }) } results.set(key, next) } } const current = results.get(key) if (current instanceof Promise) { throw current } return current }