one
Version:
One is a new React Framework that makes Vite serve both native and web.
294 lines (259 loc) • 8.6 kB
text/typescript
import { useCallback, useDeferredValue, useSyncExternalStore } from 'react'
import { useParams, usePathname } from './hooks'
import { preloadedLoaderData, 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()
// use just the pathname for matching, don't use resolveHref which adds params as query string
// (the pathname is already resolved like /docs/getting-started, not /docs/[slug])
const currentPath = pathname.replace(/\/index$/, '').replace(/\/$/, '') || '/'
// server-side only - typeof window check ensures this never runs on client
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/SSG - only use if server context path matches current path
const serverContextPath = loaderPropsFromServerContext?.path
const preloadedData = serverContextPath === 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 already-resolved preloaded data first (synchronous)
const resolvedPreloadData = preloadedLoaderData[currentPath]
if (resolvedPreloadData !== undefined) {
// Data was preloaded and already resolved - use it directly
delete preloadedLoaderData[currentPath]
delete preloadingLoader[currentPath]
loaderStateEntry.data = resolvedPreloadData
loaderStateEntry.hasLoadedOnce = true
} else if (preloadingLoader[currentPath]) {
// Preload is in progress - wait for it
const preloadPromise = preloadingLoader[currentPath]!
const promise = preloadPromise
.then((val: any) => {
delete preloadingLoader[currentPath]
delete preloadedLoaderData[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
}