UNPKG

one

Version:

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

286 lines (251 loc) 7.87 kB
import { useCallback, useSyncExternalStore } from 'react' import { 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' type LoaderStateEntry = { data: any error: any promise?: Promise<void> state: 'idle' | 'loading' timestamp?: number hasLoadedOnce?: boolean } const loaderState: Record<string, LoaderStateEntry> = {} const subscribers = new Set<() => void>() function updateState(path: string, updates: Partial<LoaderStateEntry>) { loaderState[path] = { ...loaderState[path], ...updates } subscribers.forEach((callback) => { callback() }) } function subscribe(callback: () => void) { subscribers.add(callback) return () => subscribers.delete(callback) } function getLoaderState(path: string, preloadedData?: any): LoaderStateEntry { if (!loaderState[path]) { loaderState[path] = { data: preloadedData, error: undefined, promise: undefined, state: 'idle', hasLoadedOnce: !!preloadedData, } } return loaderState[path] } export async function refetchLoader(pathname: string): Promise<void> { updateState(pathname, { state: 'loading', error: null, }) try { const cacheBust = `${Date.now()}` const loaderJSUrl = getLoaderPath(pathname, true, cacheBust) const module = await dynamicImport(loaderJSUrl) const result = await module.loader() updateState(pathname, { data: result, state: 'idle', timestamp: Date.now(), hasLoadedOnce: true, }) } catch (err) { updateState(pathname, { error: err, state: 'idle', }) throw err } } export function useLoaderState< Loader extends Function = any, Returned = Loader extends (p: any) => any ? ReturnType<Loader> : unknown, >( loader?: Loader ): Loader extends undefined ? { refetch: () => Promise<void>; state: 'idle' | 'loading' } : { data: Returned extends Promise<any> ? Awaited<Returned> : Returned refetch: () => Promise<void> state: 'idle' | 'loading' } { const { loaderProps: loaderPropsFromServerContext, loaderData: loaderDataFromServerContext } = useServerContext() || {} const params = useParams() const pathname = usePathname() const currentPath = resolveHref({ pathname, params }).replace(/index$/, '') // server-side if (typeof window === 'undefined' && loader) { const serverData = useAsyncFn( loader, loaderPropsFromServerContext || { path: pathname, params, } ) return { data: serverData, refetch: async () => {}, state: 'idle' } as any } // preloaded data from SSR const preloadedData = loaderPropsFromServerContext?.path === currentPath ? loaderDataFromServerContext : undefined const loaderStateEntry = useSyncExternalStore( subscribe, () => getLoaderState(currentPath, preloadedData), () => getLoaderState(currentPath, preloadedData) ) const refetch = useCallback(() => refetchLoader(currentPath), [currentPath]) // no loader, just return state/refetch for the path if (!loader) { return { refetch, state: loaderStateEntry.state, } as any } // start initial load if needed if ( !loaderStateEntry.data && !loaderStateEntry.promise && !loaderStateEntry.hasLoadedOnce && loader ) { // check for preloading loader first if (preloadingLoader[currentPath]) { if (typeof preloadingLoader[currentPath] === 'function') { preloadingLoader[currentPath] = preloadingLoader[currentPath]() } const promise = preloadingLoader[currentPath] .then((val: any) => { delete preloadingLoader[currentPath] updateState(currentPath, { data: val, hasLoadedOnce: true, promise: undefined, }) }) .catch((err: any) => { console.error(`Error running loader()`, err) delete preloadingLoader[currentPath] updateState(currentPath, { error: err, promise: undefined, }) }) loaderStateEntry.promise = promise } else { // initial load const loadData = async () => { try { if (process.env.TAMAGUI_TARGET === 'native') { const loaderJSUrl = getLoaderPath(currentPath, true) const nativeLoaderJSUrl = `${loaderJSUrl}?platform=ios` try { 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 need eval for native const result = eval(`() => { var exports = {}; ${loaderJsCode}; return exports; }`)() if (typeof result.loader !== 'function') { throw new Error("Loader code isn't exporting a `loader` function") } const data = await result.loader() updateState(currentPath, { data, hasLoadedOnce: true, promise: undefined, }) return } catch (e) { updateState(currentPath, { data: {}, promise: undefined, }) return } } // web platform const loaderJSUrl = getLoaderPath(currentPath, true) const module = await dynamicImport(loaderJSUrl) const result = await module.loader() updateState(currentPath, { data: result, hasLoadedOnce: true, promise: undefined, }) } catch (err) { updateState(currentPath, { error: err, promise: undefined, }) } } const promise = loadData() loaderStateEntry.promise = promise } } // handle errors and suspension if (loader) { // only throw error on initial load if (loaderStateEntry.error && !loaderStateEntry.hasLoadedOnce) { throw loaderStateEntry.error } // only throw promise for suspension on initial load if ( loaderStateEntry.data === undefined && loaderStateEntry.promise && !loaderStateEntry.hasLoadedOnce ) { throw loaderStateEntry.promise } return { data: loaderStateEntry.data, refetch, state: loaderStateEntry.state, } as any } else { return { refetch, state: loaderStateEntry.state, } as 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 { data } = useLoaderState(loader) return data } 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 }