UNPKG

responsive-rsc

Version:

Render cached React Server Components when visiting same search params in page for highly responsive UI

235 lines (232 loc) 8.38 kB
"use strict"; "use client"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.tsx var index_exports = {}; __export(index_exports, { ResponsiveSearchParamsProvider: () => ResponsiveSearchParamsProvider, ResponsiveSuspense: () => ResponsiveSuspense, isRSCPending: () => isRSCPending, useResponsiveSearchParams: () => useResponsiveSearchParams, useSetResponsiveSearchParams: () => useSetResponsiveSearchParams }); module.exports = __toCommonJS(index_exports); // src/store.tsx var import_react = require("react"); function createStore(initialValue) { const listeners = /* @__PURE__ */ new Set(); let value = initialValue; const notify = () => { for (const listener of listeners) { listener(); } }; return { getValue() { return value; }, setValue(newValue) { if (newValue === value) { return; } value = newValue; notify(); }, subscribe(listener) { listeners.add(listener); return () => { listeners.delete(listener); }; } }; } function useStore(store) { return (0, import_react.useSyncExternalStore)(store.subscribe, store.getValue, store.getValue); } // src/lib.tsx var import_navigation = require("next/navigation"); var import_react2 = require("react"); var import_jsx_runtime = require("react/jsx-runtime"); var ResponsiveSearchParamsCtx = (0, import_react2.createContext)( void 0 ); var PageSearchParamsCtx = (0, import_react2.createContext)({}); var SetResponsiveSearchParamsCtx = (0, import_react2.createContext)( void 0 ); var IsRSCPendingCtx = (0, import_react2.createContext)(void 0); var pendingSearchParamsStore = createStore( void 0 ); function stringifySearchParams(params) { const keys = Object.keys(params).sort(); const urlSearchParams = new URLSearchParams(window.location.search); for (const key of keys) { const val = params[key]; if (val) { if (Array.isArray(val)) { for (const v of val) { urlSearchParams.append(key, v); } } else { urlSearchParams.set(key, val); } } } return urlSearchParams.toString(); } function CacheRSC(props) { const { childrenCache, isPending } = props; if (isPending && props.suspendOnTransition) { throw new Promise((resolve) => { const unsubscribe = pendingSearchParamsStore.subscribe(() => { const val = pendingSearchParamsStore.getValue(); if (!val) { resolve(); unsubscribe(); } }); }); } const cachedChildren = childrenCache.get(props.cacheKey); if (cachedChildren) { return cachedChildren; } if (!isPending) { childrenCache.set(props.cacheKey, props.children); } return props.children; } function useCacheKey(searchParamsUsed) { const responsiveSearchParams = useResponsiveSearchParams(); const cacheKey = (0, import_react2.useMemo)(() => { return searchParamsUsed.filter((key) => responsiveSearchParams[key]).map((key) => `${key}=${responsiveSearchParams[key]}`).join("&"); }, [responsiveSearchParams, searchParamsUsed]); return cacheKey; } function useIsSearchParamsPending(searchParamNames) { const pendingSearchParams = useStore(pendingSearchParamsStore); return (0, import_react2.useMemo)(() => { return !!pendingSearchParams && searchParamNames.some((key) => pendingSearchParams[key]); }, [pendingSearchParams, searchParamNames]); } function ResponsiveSearchParamsProvider(props) { const pathname = (0, import_navigation.usePathname)(); const router = (0, import_navigation.useRouter)(); const [isRoutePending, startRouteTransition] = (0, import_react2.useTransition)(); const pageSearchParams = props.value; const visitedSearchParamsRef = (0, import_react2.useRef)(/* @__PURE__ */ new Set()); const [searchParamsOverride, setSearchParamsOverride] = (0, import_react2.useState)(void 0); (0, import_react2.useEffect)(() => { if (!isRoutePending) { pendingSearchParamsStore.setValue(void 0); } }, [isRoutePending]); const responsiveSearchParams = (0, import_react2.useMemo)(() => { return { ...pageSearchParams, ...searchParamsOverride }; }, [searchParamsOverride, pageSearchParams]); const setResponsiveSearchParams = (0, import_react2.useCallback)( (newSearchParamsDispatch) => { const newSearchParams = typeof newSearchParamsDispatch === "function" ? newSearchParamsDispatch(responsiveSearchParams) : newSearchParamsDispatch; setSearchParamsOverride(newSearchParams); const searchParamsString = stringifySearchParams(newSearchParams); if (visitedSearchParamsRef.current.has(searchParamsString)) { window.history.pushState({}, "", `${pathname}?${searchParamsString}`); return; } visitedSearchParamsRef.current.add(searchParamsString); const _pendingSearchParams = {}; for (const key in newSearchParams) { if (newSearchParams[key]?.toString() !== responsiveSearchParams[key]?.toString()) { _pendingSearchParams[key] = newSearchParams[key]; } } pendingSearchParamsStore.setValue(_pendingSearchParams); startRouteTransition(() => { router.replace( `${pathname}${searchParamsString ? `?${searchParamsString}` : ""}`, { scroll: false } ); }); }, [pathname, router, responsiveSearchParams] ); return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ResponsiveSearchParamsCtx.Provider, { value: responsiveSearchParams, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PageSearchParamsCtx.Provider, { value: pageSearchParams, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)( SetResponsiveSearchParamsCtx.Provider, { value: setResponsiveSearchParams, children: props.children } ) }) }); } function ResponsiveSuspense(props) { const cacheKey = useCacheKey(props.searchParamsUsed); const [childrenCache] = (0, import_react2.useState)(() => /* @__PURE__ */ new Map()); const isPending = useIsSearchParamsPending(props.searchParamsUsed); return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react2.Suspense, { fallback: props.fallback, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(IsRSCPendingCtx.Provider, { value: isPending, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)( CacheRSC, { isPending, suspendOnTransition: props.suspendOnTransition === void 0 ? true : props.suspendOnTransition, searchParamsUsed: props.searchParamsUsed, cacheKey, childrenCache, children: props.children } ) }) }); } function isRSCPending() { const val = (0, import_react2.useContext)(IsRSCPendingCtx); if (val === void 0) { throw new Error("isRSCPending must be used within a <ResponsiveSuspense>"); } return val; } function useResponsiveSearchParams() { const val = (0, import_react2.useContext)(ResponsiveSearchParamsCtx); if (val === void 0) { throw new Error( "useResponsiveSearchParams must be used within a <ResponsiveSearchParamsProvider>" ); } return val; } function useSetResponsiveSearchParams() { const val = (0, import_react2.useContext)(SetResponsiveSearchParamsCtx); if (val === void 0) { throw new Error( "useSetResponsiveSearchParams must be used within a <ResponsiveSearchParamsProvider>" ); } return val; } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { ResponsiveSearchParamsProvider, ResponsiveSuspense, isRSCPending, useResponsiveSearchParams, useSetResponsiveSearchParams }); //# sourceMappingURL=index.js.map