rwsdk
Version:
Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime
116 lines (115 loc) • 4.13 kB
JavaScript
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 });
}