UNPKG

responsive-rsc

Version:

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

215 lines (213 loc) 6.68 kB
"use client"; // src/store.tsx import { useSyncExternalStore } from "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 useSyncExternalStore(store.subscribe, store.getValue, store.getValue); } // src/lib.tsx import { usePathname, useRouter } from "next/navigation"; import { Suspense, createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, useTransition } from "react"; import { jsx } from "react/jsx-runtime"; var ResponsiveSearchParamsCtx = createContext( void 0 ); var PageSearchParamsCtx = createContext({}); var SetResponsiveSearchParamsCtx = createContext( void 0 ); var IsRSCPendingCtx = 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 = 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 useMemo(() => { return !!pendingSearchParams && searchParamNames.some((key) => pendingSearchParams[key]); }, [pendingSearchParams, searchParamNames]); } function ResponsiveSearchParamsProvider(props) { const pathname = usePathname(); const router = useRouter(); const [isRoutePending, startRouteTransition] = useTransition(); const pageSearchParams = props.value; const visitedSearchParamsRef = useRef(/* @__PURE__ */ new Set()); const [searchParamsOverride, setSearchParamsOverride] = useState(void 0); useEffect(() => { if (!isRoutePending) { pendingSearchParamsStore.setValue(void 0); } }, [isRoutePending]); const responsiveSearchParams = useMemo(() => { return { ...pageSearchParams, ...searchParamsOverride }; }, [searchParamsOverride, pageSearchParams]); const setResponsiveSearchParams = 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__ */ jsx(ResponsiveSearchParamsCtx.Provider, { value: responsiveSearchParams, children: /* @__PURE__ */ jsx(PageSearchParamsCtx.Provider, { value: pageSearchParams, children: /* @__PURE__ */ jsx( SetResponsiveSearchParamsCtx.Provider, { value: setResponsiveSearchParams, children: props.children } ) }) }); } function ResponsiveSuspense(props) { const cacheKey = useCacheKey(props.searchParamsUsed); const [childrenCache] = useState(() => /* @__PURE__ */ new Map()); const isPending = useIsSearchParamsPending(props.searchParamsUsed); return /* @__PURE__ */ jsx(Suspense, { fallback: props.fallback, children: /* @__PURE__ */ jsx(IsRSCPendingCtx.Provider, { value: isPending, children: /* @__PURE__ */ jsx( CacheRSC, { isPending, suspendOnTransition: props.suspendOnTransition === void 0 ? true : props.suspendOnTransition, searchParamsUsed: props.searchParamsUsed, cacheKey, childrenCache, children: props.children } ) }) }); } function isRSCPending() { const val = useContext(IsRSCPendingCtx); if (val === void 0) { throw new Error("isRSCPending must be used within a <ResponsiveSuspense>"); } return val; } function useResponsiveSearchParams() { const val = useContext(ResponsiveSearchParamsCtx); if (val === void 0) { throw new Error( "useResponsiveSearchParams must be used within a <ResponsiveSearchParamsProvider>" ); } return val; } function useSetResponsiveSearchParams() { const val = useContext(SetResponsiveSearchParamsCtx); if (val === void 0) { throw new Error( "useSetResponsiveSearchParams must be used within a <ResponsiveSearchParamsProvider>" ); } return val; } export { ResponsiveSearchParamsProvider, ResponsiveSuspense, isRSCPending, useResponsiveSearchParams, useSetResponsiveSearchParams }; //# sourceMappingURL=index.mjs.map