one
Version:
One is a new React Framework that makes Vite serve both native and web.
286 lines (251 loc) • 7.87 kB
text/typescript
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
}