|
| 1 | +'use client' |
| 2 | +import { use, useState, useEffect } from 'react' |
| 3 | +import { useQueryClient, useQuery, dehydrate, HydrationBoundary } from '@tanstack/react-query' |
| 4 | +import type { UniversalRouteProps, QueryFn } from './UniversalRouteScreen.helpers' |
| 5 | + |
| 6 | +/* --- Helpers --------------------------------------------------------------------------------- */ |
| 7 | + |
| 8 | +const getSSRData = () => { |
| 9 | + const $ssrData = document.getElementById('ssr-data') |
| 10 | + const ssrDataText = $ssrData?.getAttribute('data-ssr') |
| 11 | + const ssrData = ssrDataText ? (JSON.parse(ssrDataText) as Record<string, any>) : null |
| 12 | + return ssrData |
| 13 | +} |
| 14 | + |
| 15 | +const getDehydratedSSRState = () => { |
| 16 | + const $ssrHydrationState = document.getElementById('ssr-hydration-state') |
| 17 | + const ssrHydrationStateText = $ssrHydrationState?.getAttribute('data-ssr') |
| 18 | + const ssrHydrationState = ssrHydrationStateText ? (JSON.parse(ssrHydrationStateText) as Record<string, any>) : null |
| 19 | + return ssrHydrationState |
| 20 | +} |
| 21 | + |
| 22 | +/** --- <UniversalRouteScreen/> ---------------------------------------------------------------- */ |
| 23 | +/** -i- Universal Route Wrapper to provide query data on mobile, the browser and during server rendering */ |
| 24 | +export const UniversalRouteScreen = <Fetcher extends QueryFn>(props: UniversalRouteProps<Fetcher>) => { |
| 25 | + // Props |
| 26 | + const { params: routeParams, searchParams, queryBridge, routeScreen: RouteScreen, ...screenProps } = props |
| 27 | + const { routeParamsToQueryKey, routeParamsToQueryInput, routeDataFetcher } = queryBridge |
| 28 | + const fetcherDataToProps = queryBridge.fetcherDataToProps || ((data: ReturnType<Fetcher>) => data) |
| 29 | + |
| 30 | + // Context |
| 31 | + const queryClient = useQueryClient() |
| 32 | + |
| 33 | + // State |
| 34 | + const [hydratedData, setHydratedData] = useState<Record<string, any> | null>(null) |
| 35 | + const [hydratedQueries, setHydratedQueries] = useState<Record<string, any> | null>(null) |
| 36 | + |
| 37 | + // Vars |
| 38 | + const isBrowser = typeof window !== 'undefined' |
| 39 | + const queryParams = { ...routeParams, ...searchParams } |
| 40 | + const queryKey = routeParamsToQueryKey(queryParams) |
| 41 | + const queryInput = routeParamsToQueryInput(queryParams) |
| 42 | + |
| 43 | + // -- Effects -- |
| 44 | + |
| 45 | + useEffect(() => { |
| 46 | + const ssrData = getSSRData() |
| 47 | + if (ssrData) setHydratedData(ssrData) // Save the SSR data to state, removing the SSR data from the DOM |
| 48 | + const hydratedQueyClientState = getDehydratedSSRState() |
| 49 | + if (hydratedQueyClientState) setHydratedQueries(hydratedQueyClientState) // Save the hydrated state to state, removing the hydrated state from the DOM |
| 50 | + }, []) |
| 51 | + |
| 52 | + // -- Query -- |
| 53 | + |
| 54 | + const queryConfig = { |
| 55 | + queryKey, |
| 56 | + queryFn: async () => await routeDataFetcher(queryInput), |
| 57 | + initialData: queryBridge.initialData, |
| 58 | + } |
| 59 | + |
| 60 | + // -- Browser -- |
| 61 | + |
| 62 | + if (isBrowser) { |
| 63 | + const hydrationData = hydratedData || getSSRData() |
| 64 | + const hydrationState = hydratedQueries || getDehydratedSSRState() |
| 65 | + const renderHydrationData = !!hydrationData && !hydratedData // Only render the hydration data if it's not already in state |
| 66 | + |
| 67 | + const { data: fetcherData } = useQuery({ |
| 68 | + ...queryConfig, |
| 69 | + initialData: { |
| 70 | + ...queryConfig.initialData, |
| 71 | + ...hydrationData, |
| 72 | + }, |
| 73 | + }) |
| 74 | + const routeDataProps = fetcherDataToProps(fetcherData as Awaited<ReturnType<Fetcher>>) as Record<string, unknown> // prettier-ignore |
| 75 | + |
| 76 | + return ( |
| 77 | + <HydrationBoundary state={hydrationState}> |
| 78 | + {renderHydrationData && <div id="ssr-data" data-ssr={JSON.stringify(fetcherData)} />} |
| 79 | + {renderHydrationData && <div id="ssr-hydration-state" data-ssr={JSON.stringify(hydrationState)} />} |
| 80 | + <RouteScreen |
| 81 | + {...routeDataProps} |
| 82 | + queryKey={queryKey} |
| 83 | + queryInput={queryInput} |
| 84 | + {...screenProps} // @ts-ignore |
| 85 | + params={routeParams} |
| 86 | + searchParams={searchParams} |
| 87 | + /> |
| 88 | + </HydrationBoundary> |
| 89 | + ) |
| 90 | + } |
| 91 | + |
| 92 | + // -- Server -- |
| 93 | + |
| 94 | + const fetcherData = use(queryClient.fetchQuery(queryConfig)) as Awaited<ReturnType<Fetcher>> |
| 95 | + const routeDataProps = fetcherDataToProps(fetcherData) as Record<string, unknown> |
| 96 | + const dehydratedState = dehydrate(queryClient) |
| 97 | + |
| 98 | + console.log('Server:', JSON.stringify({ routeParams, searchParams, queryParams, queryInput, fetcherData, routeDataProps }, null, 2)) |
| 99 | + |
| 100 | + return ( |
| 101 | + <HydrationBoundary state={dehydratedState}> |
| 102 | + {!!fetcherData && <div id="ssr-data" data-ssr={JSON.stringify(fetcherData)} />} |
| 103 | + {!!dehydratedState && <div id="ssr-hydration-state" data-ssr={JSON.stringify(dehydratedState)} />} |
| 104 | + <RouteScreen |
| 105 | + {...routeDataProps} |
| 106 | + queryKey={queryKey} |
| 107 | + queryInput={queryInput} |
| 108 | + {...screenProps} // @ts-ignore |
| 109 | + params={routeParams} |
| 110 | + searchParams={searchParams} |
| 111 | + /> |
| 112 | + </HydrationBoundary> |
| 113 | + ) |
| 114 | +} |
0 commit comments