diff --git a/src/lib/query/README.md b/src/lib/query/README.md new file mode 100644 index 00000000..8d328d0f --- /dev/null +++ b/src/lib/query/README.md @@ -0,0 +1,119 @@ +# TanStack Query SSR Integration + +This directory contains utilities for integrating TanStack Query v6 with SvelteKit's server-side rendering. + +## Architecture Overview + +The project uses a hybrid approach for SSR: + +1. **QueryClient in Layout**: The `QueryClient` is created in `+layout.ts` and passed to `+layout.svelte` via the load function. This enables prefetching in child page load functions. + +2. **Server Data with initialData**: Pages that use `+page.server.ts` can pass server-fetched data as `initialData` to TanStack Query using the `withInitialData()` helper. + +3. **Prefetching in +page.ts**: Pages that use `+page.ts` (universal load functions) can use `prefetchQuery()` to populate the QueryClient cache before rendering. + +## Usage Examples + +### Pattern 1: Using Server Data with initialData + +For pages that already fetch data in `+page.server.ts`: + +```svelte + + + +{#if $party.data} +

{$party.data.name}

+{/if} +``` + +### Pattern 2: Prefetching in Universal Load Functions + +For pages that can use `+page.ts` (not server-only): + +```typescript +// +page.ts +import type { PageLoad } from './$types' +import { prefetchQuery } from '$lib/query/ssr' +import { partyQueries } from '$lib/api/queries/party.queries' + +export const load: PageLoad = async ({ parent, params }) => { + const { queryClient } = await parent() + + // Prefetch party data into the cache + await prefetchQuery(queryClient, partyQueries.byShortcode(params.id)) + + // No need to return data - it's already in the QueryClient cache + return { shortcode: params.id } +} +``` + +```svelte + + + +{#if $party.data} +

{$party.data.name}

+{/if} +``` + +### Pattern 3: Infinite Queries with Prefetching + +For paginated data: + +```typescript +// +page.ts +import type { PageLoad } from './$types' +import { prefetchInfiniteQuery } from '$lib/query/ssr' +import { partyQueries } from '$lib/api/queries/party.queries' + +export const load: PageLoad = async ({ parent }) => { + const { queryClient } = await parent() + + // Prefetch first page of parties + await prefetchInfiniteQuery(queryClient, partyQueries.list()) +} +``` + +## Migration Guide + +### From Server-Only to TanStack Query + +1. **Keep existing +page.server.ts** - No changes needed to server load functions +2. **Add TanStack Query to component** - Use `createQuery` with `withInitialData` +3. **Benefit from caching** - Subsequent navigations use cached data + +### From Custom Resources to TanStack Query + +1. **Replace resource imports** with query/mutation imports +2. **Use createQuery** instead of resource state +3. **Use mutations** for CRUD operations with automatic cache invalidation + +## Files + +- `queryClient.ts` - QueryClient factory (legacy, kept for reference) +- `ssr.ts` - SSR utilities (withInitialData, prefetchQuery, etc.) diff --git a/src/lib/query/ssr.ts b/src/lib/query/ssr.ts new file mode 100644 index 00000000..3ef7e822 --- /dev/null +++ b/src/lib/query/ssr.ts @@ -0,0 +1,186 @@ +/** + * SSR Utilities for TanStack Query + * + * Provides utilities for integrating server-side data fetching with TanStack Query. + * These utilities support the initialData pattern for pages that use +page.server.ts + * load functions. + * + * @module query/ssr + */ + +import type { QueryClient } from '@tanstack/svelte-query' + +/** + * Options for creating a query with initial data from SSR + */ +export interface InitialDataOptions { + /** + * The data fetched on the server to use as initial data + */ + initialData: TData | undefined | null + + /** + * Optional timestamp when the data was fetched on the server. + * If not provided, defaults to 0 (will be considered stale immediately). + * Use Date.now() on the server to get accurate timestamps. + */ + initialDataUpdatedAt?: number +} + +/** + * Creates query options with initial data from server-side rendering. + * + * Use this helper when you have data fetched in a +page.server.ts load function + * and want to use it as initial data for a TanStack Query. + * + * @example + * ```svelte + * + * ``` + * + * @param initialData - The data fetched on the server + * @param updatedAt - Optional timestamp when data was fetched (defaults to 0) + * @returns Query options object with initialData and initialDataUpdatedAt + */ +export function withInitialData( + initialData: TData | undefined | null, + updatedAt?: number +): InitialDataOptions { + return { + initialData: initialData ?? undefined, + initialDataUpdatedAt: updatedAt ?? 0 + } +} + +/** + * Prefetches a query on the server and returns the data. + * + * Use this in +page.ts load functions when you want to prefetch data + * into the QueryClient cache. This is the recommended approach for + * pages that don't use +page.server.ts. + * + * Note: This will NOT work with +page.server.ts load functions. + * Use withInitialData() instead for server-only load functions. + * + * @example + * ```typescript + * // +page.ts + * import type { PageLoad } from './$types' + * import { prefetchQuery } from '$lib/query/ssr' + * import { partyQueries } from '$lib/api/queries/party.queries' + * + * export const load: PageLoad = async ({ parent, params }) => { + * const { queryClient } = await parent() + * + * await prefetchQuery(queryClient, partyQueries.byShortcode(params.id)) + * + * // No need to return data - it's in the cache + * } + * ``` + * + * @param queryClient - The QueryClient instance from parent layout + * @param options - Query options from a query factory + */ +export async function prefetchQuery( + queryClient: QueryClient, + options: { + queryKey: readonly unknown[] + queryFn: () => Promise + staleTime?: number + gcTime?: number + } +): Promise { + await queryClient.prefetchQuery({ + queryKey: options.queryKey, + queryFn: options.queryFn, + staleTime: options.staleTime, + gcTime: options.gcTime + }) +} + +/** + * Prefetches an infinite query on the server. + * + * Use this in +page.ts load functions when you want to prefetch + * paginated data into the QueryClient cache. + * + * @example + * ```typescript + * // +page.ts + * import type { PageLoad } from './$types' + * import { prefetchInfiniteQuery } from '$lib/query/ssr' + * import { partyQueries } from '$lib/api/queries/party.queries' + * + * export const load: PageLoad = async ({ parent }) => { + * const { queryClient } = await parent() + * + * await prefetchInfiniteQuery(queryClient, partyQueries.list()) + * } + * ``` + * + * @param queryClient - The QueryClient instance from parent layout + * @param options - Infinite query options from a query factory + */ +export async function prefetchInfiniteQuery( + queryClient: QueryClient, + options: { + queryKey: readonly unknown[] + queryFn: (context: { pageParam: number }) => Promise + initialPageParam: number + staleTime?: number + gcTime?: number + } +): Promise { + await queryClient.prefetchInfiniteQuery({ + queryKey: options.queryKey, + queryFn: options.queryFn, + initialPageParam: options.initialPageParam, + staleTime: options.staleTime, + gcTime: options.gcTime + }) +} + +/** + * Sets query data directly in the cache. + * + * Use this when you have data from a server load function and want + * to populate the QueryClient cache directly. This is useful when + * migrating from server-only load functions to TanStack Query. + * + * @example + * ```typescript + * // In a component or effect + * import { useQueryClient } from '@tanstack/svelte-query' + * import { setQueryData } from '$lib/query/ssr' + * import { partyKeys } from '$lib/api/queries/party.queries' + * + * const queryClient = useQueryClient() + * + * // Populate cache with server data + * setQueryData(queryClient, partyKeys.detail(shortcode), serverParty) + * ``` + * + * @param queryClient - The QueryClient instance + * @param queryKey - The query key to set data for + * @param data - The data to set in the cache + */ +export function setQueryData( + queryClient: QueryClient, + queryKey: readonly unknown[], + data: TData +): void { + queryClient.setQueryData(queryKey, data) +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 7b066550..b09c94fc 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -11,14 +11,12 @@ import { authStore } from '$lib/stores/auth.store' import { browser } from '$app/environment' import { QueryClientProvider } from '@tanstack/svelte-query' - import { createQueryClient } from '$lib/query/queryClient' - - const queryClient = createQueryClient() + import type { LayoutData } from './$types' // Get `data` and `children` from the router via $props() - // Use a more flexible type that allows additional properties from child pages + // QueryClient is now created in +layout.ts for SSR support const { data, children } = $props<{ - data: any // Allow any data to pass through from child pages + data: LayoutData & { [key: string]: any } // Allow any data to pass through from child pages children: () => any }>() @@ -110,7 +108,7 @@ - +
diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts new file mode 100644 index 00000000..52cf5021 --- /dev/null +++ b/src/routes/+layout.ts @@ -0,0 +1,34 @@ +/** + * Root Layout Load Function + * + * Creates a QueryClient instance for SSR support with TanStack Query v6. + * The QueryClient is created here so it can be used for prefetching in + * child page load functions. + * + * @module routes/+layout + */ + +import type { LayoutLoad } from './$types' +import { browser } from '$app/environment' +import { QueryClient } from '@tanstack/svelte-query' + +export const load: LayoutLoad = async () => { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + // Disable queries on server - they will be prefetched explicitly + enabled: browser, + // Cache data for 5 minutes before considering it stale + staleTime: 1000 * 60 * 5, + // Keep unused data in cache for 30 minutes + gcTime: 1000 * 60 * 30, + // Retry failed requests twice + retry: 2, + // Don't refetch on window focus by default + refetchOnWindowFocus: false + } + } + }) + + return { queryClient } +}