UNPKG

one

Version:

One is a React framework for web, iOS, and Android apps.

142 lines (129 loc) • 3.94 kB
import { useSyncExternalStore } from 'react' import { useServerContext } from './vite/one-server-only' import type { One } from './vite/types' /** * Re-export for convenience */ export type RouteMatch = One.RouteMatch // client-side matches store for navigation updates let clientMatches: RouteMatch[] = [] const clientMatchesListeners = new Set<() => void>() export function subscribeToClientMatches(callback: () => void) { clientMatchesListeners.add(callback) return () => clientMatchesListeners.delete(callback) } export function getClientMatchesSnapshot() { return clientMatches } /** * Update the client-side matches store. * Called after navigation to update the matches with new loader data. * @internal */ export function setClientMatches(matches: RouteMatch[]) { clientMatches = matches // route state can synchronize during a layout's render. queueMicrotask(() => { for (const listener of clientMatchesListeners) { listener() } }) } /** * Update the loaderData for a single match by routeId, leaving all other matches intact. * @internal */ export function updateMatchLoaderData(routeId: string, loaderData: unknown) { clientMatches = clientMatches.map((m) => m.routeId === routeId ? { ...m, loaderData } : m ) for (const listener of clientMatchesListeners) { listener() } } /** * Returns an array of all matched routes from root to the current page. * Each match contains the route's loader data, params, and route ID. * * On the server (SSR), this returns the matches computed during the request. * On the client, this returns cached matches from hydration or the last navigation. * * @example * ```tsx * // In a layout component * function DocsLayout({ children }) { * const matches = useMatches() * const pageMatch = matches[matches.length - 1] * const headings = (pageMatch?.loaderData as { headings?: Heading[] } | undefined)?.headings * * return ( * <div> * <TableOfContents headings={headings} /> * {children} * </div> * ) * } * ``` * * @example * ```tsx * // Building breadcrumbs * function Breadcrumbs() { * const matches = useMatches() * * return ( * <nav> * {matches.map((match) => ( * <a key={match.routeId} href={match.pathname}> * {(match.loaderData as { title?: string } | undefined)?.title ?? match.routeId} * </a> * ))} * </nav> * ) * } * ``` */ export function useMatches(): RouteMatch[] { const serverContext = useServerContext() // on server, return from context directly if (process.env.VITE_ENVIRONMENT === 'ssr') { return serverContext?.matches ?? [] } // on client, use sync external store for reactivity // the server snapshot (3rd arg) is used during hydration to match SSR output const clientStoreMatches = useSyncExternalStore( subscribeToClientMatches, getClientMatchesSnapshot, // server snapshot for hydration - must match what SSR rendered () => serverContext?.matches ?? [] ) // always return client store on client - it's initialized from server context // during hydration via initClientMatches, then updated on navigation return clientStoreMatches } /** * Find a specific match by route ID. * * @example * ```tsx * const docsMatch = useMatch('./docs/_layout.tsx') * const navItems = (docsMatch?.loaderData as DocsLayoutData | undefined)?.navItems * ``` */ export function useMatch(routeId: string): RouteMatch | undefined { const matches = useMatches() return matches.find((m) => m.routeId === routeId) } /** * Get the current page's match (the last/deepest match). * * @example * ```tsx * const pageMatch = usePageMatch() * const { title, description } = (pageMatch?.loaderData as PageMeta | undefined) ?? {} * ``` */ export function usePageMatch(): RouteMatch | undefined { const matches = useMatches() return matches[matches.length - 1] }