UNPKG

responsive-rsc

Version:

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

1 lines 13 kB
{"version":3,"sources":["../src/store.tsx","../src/lib.tsx"],"sourcesContent":["\"use client\";\n\nimport { useSyncExternalStore } from \"react\";\n\nexport type Store<T> = {\n getValue(): T;\n setValue(newValue: T): void;\n subscribe(listener: () => void): () => void;\n};\n\nexport function createStore<T>(initialValue: T): Store<T> {\n type Listener = () => void;\n const listeners = new Set<Listener>();\n\n let value = initialValue;\n\n const notify = () => {\n for (const listener of listeners) {\n listener();\n }\n };\n\n return {\n getValue() {\n return value;\n },\n setValue(newValue: T) {\n if (newValue === value) {\n return;\n }\n value = newValue;\n notify();\n },\n subscribe(listener: Listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n}\n\nexport function useStore<T>(store: Store<T>): T {\n return useSyncExternalStore(store.subscribe, store.getValue, store.getValue);\n}\n","\"use client\";\n\nimport { createStore, useStore } from \"./store\";\nimport { usePathname, useRouter } from \"next/navigation\";\nimport {\n Suspense,\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n useTransition,\n} from \"react\";\n\n// General types -------------------------------------\n\nexport type SearchParams = Record<string, string | string[] | undefined>;\nexport type SetSearchParams = React.Dispatch<\n React.SetStateAction<SearchParams>\n>;\n\n// contexts --------------------------------------------\n\nconst ResponsiveSearchParamsCtx = createContext<SearchParams | undefined>(\n undefined\n);\n\nconst PageSearchParamsCtx = createContext<SearchParams>({});\n\nconst SetResponsiveSearchParamsCtx = createContext<SetSearchParams | undefined>(\n undefined\n);\n\nconst IsRSCPendingCtx = createContext<boolean | undefined>(undefined);\n\n// stores ----------------------------------------------\n\nconst pendingSearchParamsStore = createStore<SearchParams | undefined>(\n undefined\n);\n\n// Internals --------------------------------\n\nfunction stringifySearchParams(params: SearchParams) {\n const keys = Object.keys(params).sort();\n const urlSearchParams = new URLSearchParams(window.location.search);\n\n for (const key of keys) {\n const val = params[key];\n if (val) {\n if (Array.isArray(val)) {\n for (const v of val) {\n urlSearchParams.append(key, v);\n }\n } else {\n urlSearchParams.set(key, val);\n }\n }\n }\n\n return urlSearchParams.toString();\n}\n\nfunction CacheRSC(props: {\n searchParamsUsed: string[];\n children: React.ReactNode;\n cacheKey: string;\n childrenCache: Map<string, React.ReactNode>;\n suspendOnTransition: boolean;\n isPending: boolean;\n}) {\n const { childrenCache, isPending } = props;\n\n if (isPending && props.suspendOnTransition) {\n throw new Promise<void>((resolve) => {\n const unsubscribe = pendingSearchParamsStore.subscribe(() => {\n const val = pendingSearchParamsStore.getValue();\n if (!val) {\n resolve();\n unsubscribe();\n }\n });\n });\n }\n\n const cachedChildren = childrenCache.get(props.cacheKey);\n\n if (cachedChildren) {\n return cachedChildren;\n }\n\n if (!isPending) {\n childrenCache.set(props.cacheKey, props.children);\n }\n\n return props.children;\n}\n\nfunction useCacheKey(searchParamsUsed: string[]) {\n const responsiveSearchParams = useResponsiveSearchParams();\n\n const cacheKey = useMemo(() => {\n return searchParamsUsed\n .filter((key) => responsiveSearchParams[key])\n .map((key) => `${key}=${responsiveSearchParams[key]}`)\n .join(\"&\");\n }, [responsiveSearchParams, searchParamsUsed]);\n\n return cacheKey;\n}\n\nfunction useIsSearchParamsPending(searchParamNames: string[]) {\n const pendingSearchParams = useStore(pendingSearchParamsStore);\n return useMemo(() => {\n return (\n !!pendingSearchParams &&\n searchParamNames.some((key) => pendingSearchParams[key])\n );\n }, [pendingSearchParams, searchParamNames]);\n}\n\n// Components -----------------------------------------\n\nexport type ResponsiveSearchParamsProviderProps = {\n children: React.ReactNode;\n value: SearchParams;\n};\n\nexport function ResponsiveSearchParamsProvider(\n props: ResponsiveSearchParamsProviderProps\n) {\n const pathname = usePathname();\n const router = useRouter();\n const [isRoutePending, startRouteTransition] = useTransition();\n\n const pageSearchParams = props.value;\n const visitedSearchParamsRef = useRef(new Set<string>());\n\n const [searchParamsOverride, setSearchParamsOverride] = useState<\n SearchParams | undefined\n >(undefined);\n\n useEffect(() => {\n if (!isRoutePending) {\n pendingSearchParamsStore.setValue(undefined);\n }\n }, [isRoutePending]);\n\n const responsiveSearchParams = useMemo(() => {\n return {\n ...pageSearchParams,\n ...searchParamsOverride,\n };\n }, [searchParamsOverride, pageSearchParams]);\n\n const setResponsiveSearchParams: SetSearchParams = useCallback(\n (newSearchParamsDispatch) => {\n const newSearchParams =\n typeof newSearchParamsDispatch === \"function\"\n ? newSearchParamsDispatch(responsiveSearchParams)\n : newSearchParamsDispatch;\n\n setSearchParamsOverride(newSearchParams);\n const searchParamsString = stringifySearchParams(newSearchParams);\n\n // if this search params was already visited\n if (visitedSearchParamsRef.current.has(searchParamsString)) {\n // update window without reloading\n window.history.pushState({}, \"\", `${pathname}?${searchParamsString}`);\n return;\n }\n\n // if this search params is new\n visitedSearchParamsRef.current.add(searchParamsString);\n\n // calculate pending search params by comparing new search params with current search params\n const _pendingSearchParams: SearchParams = {};\n for (const key in newSearchParams) {\n if (\n newSearchParams[key]?.toString() !==\n responsiveSearchParams[key]?.toString()\n ) {\n _pendingSearchParams[key] = newSearchParams[key];\n }\n }\n\n pendingSearchParamsStore.setValue(_pendingSearchParams);\n\n startRouteTransition(() => {\n router.replace(\n `${pathname}${searchParamsString ? `?${searchParamsString}` : \"\"}`,\n {\n scroll: false,\n }\n );\n });\n },\n [pathname, router, responsiveSearchParams]\n );\n\n return (\n <ResponsiveSearchParamsCtx.Provider value={responsiveSearchParams}>\n <PageSearchParamsCtx.Provider value={pageSearchParams}>\n <SetResponsiveSearchParamsCtx.Provider\n value={setResponsiveSearchParams}\n >\n {props.children}\n </SetResponsiveSearchParamsCtx.Provider>\n </PageSearchParamsCtx.Provider>\n </ResponsiveSearchParamsCtx.Provider>\n );\n}\n\nexport type ResponsiveSuspenseProps = {\n searchParamsUsed: string[];\n children: React.ReactNode;\n fallback: React.ReactNode;\n suspendOnTransition?: boolean;\n};\n\nexport function ResponsiveSuspense(props: ResponsiveSuspenseProps) {\n const cacheKey = useCacheKey(props.searchParamsUsed);\n const [childrenCache] = useState(() => new Map<string, React.ReactNode>());\n const isPending = useIsSearchParamsPending(props.searchParamsUsed);\n\n return (\n <Suspense fallback={props.fallback}>\n <IsRSCPendingCtx.Provider value={isPending}>\n <CacheRSC\n isPending={isPending}\n suspendOnTransition={\n props.suspendOnTransition === undefined\n ? true\n : props.suspendOnTransition\n }\n searchParamsUsed={props.searchParamsUsed}\n cacheKey={cacheKey}\n childrenCache={childrenCache}\n >\n {props.children}\n </CacheRSC>\n </IsRSCPendingCtx.Provider>\n </Suspense>\n );\n}\n\n// Hooks ----------------------------------------------\n\nexport function isRSCPending() {\n const val = useContext(IsRSCPendingCtx);\n if (val === undefined) {\n throw new Error(\"isRSCPending must be used within a <ResponsiveSuspense>\");\n }\n\n return val;\n}\n\nexport function useResponsiveSearchParams() {\n const val = useContext(ResponsiveSearchParamsCtx);\n if (val === undefined) {\n throw new Error(\n \"useResponsiveSearchParams must be used within a <ResponsiveSearchParamsProvider>\"\n );\n }\n\n return val;\n}\n\nexport function useSetResponsiveSearchParams() {\n const val = useContext(SetResponsiveSearchParamsCtx);\n if (val === undefined) {\n throw new Error(\n \"useSetResponsiveSearchParams must be used within a <ResponsiveSearchParamsProvider>\"\n );\n }\n return val;\n}\n"],"mappings":"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","names":[]}