UNPKG

rwsdk

Version:

Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime

116 lines (115 loc) 4.13 kB
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime"; import React, { useContext, useSyncExternalStore } from "react"; import { getNavigationSnapshot, isSameNavigationDocumentUrl, subscribeNavigationState, } from "./navigationState.js"; const NO_NAVIGATION_PAYLOAD_META = Symbol("NO_NAVIGATION_PAYLOAD_META"); const NavigationPayloadMetaContext = React.createContext(NO_NAVIGATION_PAYLOAD_META); export function NavigationPayloadProvider({ children, meta, }) { return (_jsx(NavigationPayloadMetaContext.Provider, { value: meta, children: children })); } function cloneUrl(url) { return new URL(url.href); } function arraysEqual(left, right) { if (left.length !== right.length) { return false; } return left.every((value, index) => value === right[index]); } function searchParamChanged(currentUrl, pendingUrl, name) { return !arraysEqual(currentUrl.searchParams.getAll(name), pendingUrl.searchParams.getAll(name)); } function searchParamsChanged(currentUrl, pendingUrl, watch) { if (watch === false) { return false; } if (watch === true) { return currentUrl.search !== pendingUrl.search; } return watch.some((name) => searchParamChanged(currentUrl, pendingUrl, name)); } function watchedUrlChanged(currentUrl, pendingUrl, watch) { const watchPathname = watch.pathname ?? true; const watchSearchParams = watch.searchParams ?? true; const watchHash = watch.hash ?? false; if (watchPathname && currentUrl.pathname !== pendingUrl.pathname) { return true; } if (searchParamsChanged(currentUrl, pendingUrl, watchSearchParams)) { return true; } if (watchHash && currentUrl.hash !== pendingUrl.hash) { return true; } return false; } function isRenderingMatchingNavigationPayload(snapshot, meta) { const pending = snapshot.pending; if (!pending) { return false; } if (meta === NO_NAVIGATION_PAYLOAD_META) { return false; } if (meta === undefined) { return true; } if (meta.source !== "navigation") { return false; } if (!meta.href) { return true; } try { return isSameNavigationDocumentUrl(new URL(meta.href, pending.currentUrl), pending.pendingUrl); } catch { return false; } } export function shouldSuspendForPendingNavigation(snapshot, options = {}) { const pending = snapshot.pending; if (!pending) { return false; } const currentUrl = pending.currentUrl; const pendingUrl = pending.pendingUrl; if (options.when) { return options.when({ currentUrl: cloneUrl(currentUrl), pendingUrl: cloneUrl(pendingUrl), }); } if (options.watch) { return watchedUrlChanged(currentUrl, pendingUrl, options.watch); } if (options.searchParams) { return searchParamsChanged(currentUrl, pendingUrl, options.searchParams); } return true; } /** * Suspends while a matching client-side RSC navigation is pending. * * Use this hook inside a React <Suspense> boundary. When the pending navigation * commits to the visible React tree, the thrown promise resolves and React * retries the render with the newly committed tree. */ export function useNavigationPending(options = {}) { const snapshot = useSyncExternalStore(subscribeNavigationState, getNavigationSnapshot, getNavigationSnapshot); const payloadMeta = useContext(NavigationPayloadMetaContext); if (shouldSuspendForPendingNavigation(snapshot, options) && !isRenderingMatchingNavigationPayload(snapshot, payloadMeta)) { throw snapshot.pending.promise; } return snapshot; } /** * Suspense-aware boundary for client-side RSC navigations. * * Wrap server-backed UI with this component inside your own <Suspense> fallback * to avoid showing stale props while a relevant navigation is in flight. */ export function NavigationPending({ children, searchParams, watch, when, }) { useNavigationPending({ searchParams, watch, when }); return _jsx(_Fragment, { children: children }); }