--- id: ssr title: Server Rendering & Hydration --- Lit Query can be used with server rendering by combining Lit SSR with TanStack Query Core hydration APIs re-exported from `@tanstack/lit-query`. The runnable source for this guide is the [SSR example](../examples/ssr). ## Flow Server rendering has three phases: 1. Create a per-request `QueryClient`. 2. Prefetch queries on the server and render Lit HTML with that client. 3. Dehydrate the cache into the HTML, then hydrate a browser `QueryClient` before the client app renders. Never share one server `QueryClient` between users or requests. ## Server Prefetch and Render ```ts import { render } from '@lit-labs/ssr' import { collectResult } from '@lit-labs/ssr/lib/render-result.js' import { html } from 'lit' import { QueryClient, dehydrate } from '@tanstack/lit-query' import { createDataQueryOptions } from './api.js' import './app.js' async function renderPage() { const apiBaseUrl = 'https://example.com' const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 30_000, }, }, }) await queryClient.prefetchQuery(createDataQueryOptions(apiBaseUrl)) const appHtml = await collectResult( render( html``, ), ) const dehydratedState = dehydrate(queryClient) return { appHtml, dehydratedState } } ``` The server passes the same client into the Lit element with a property binding. This lets `createQueryController` read the prefetched cache during server render. If your query function calls `fetch` during SSR, pass an absolute API origin instead of relying on a browser-relative URL. ## Client Hydration ```ts import '@lit-labs/ssr-client/lit-element-hydrate-support.js' import { QueryClient, hydrate, type DehydratedState } from '@tanstack/lit-query' const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 30_000, }, }, }) const dehydratedState = JSON.parse( document.getElementById('__QUERY_STATE__')?.textContent ?? 'null', ) as DehydratedState queryClient.mount() hydrate(queryClient, dehydratedState) const appElement = document.querySelector('ssr-app') as | (HTMLElement & { queryClient?: QueryClient }) | null if (!appElement) { throw new Error('Expected the SSR app element to exist before hydration.') } appElement.queryClient = queryClient await import('./app.js') ``` Unmount the client when the page is unloaded if you mounted it manually: ```ts window.addEventListener( 'pagehide', () => { queryClient.unmount() }, { once: true }, ) ``` ## Component Pattern The SSR example creates its controller only after a `queryClient` property is available: ```ts import { LitElement } from 'lit' import { createQueryController, type QueryClient, type QueryResultAccessor, } from '@tanstack/lit-query' import { createDataQueryOptions, type DataResponse } from './api.js' class SsrApp extends LitElement { static properties = { apiBaseUrl: { attribute: 'api-base-url' }, queryClient: { attribute: false }, } apiBaseUrl = '' queryClient?: QueryClient private dataQuery?: QueryResultAccessor protected override willUpdate(): void { if (!this.dataQuery && this.queryClient) { this.dataQuery = createQueryController( this, createDataQueryOptions(this.apiBaseUrl), this.queryClient, ) } } } ``` This explicit-client pattern is useful for SSR because the client is created by the renderer rather than discovered from a connected DOM provider. ## Serialization Embed dehydrated state as JSON in the HTML and escape characters that can break out of a script tag. The example server uses a small serializer before replacing `__QUERY_STATE_JSON__` in the built HTML template. Lit Query re-exports `dehydrate` and `hydrate` from TanStack Query Core. Use `dehydrate(queryClient)` after server prefetching to capture the cache state. In the browser, parse that state, create a fresh `QueryClient`, call `hydrate(queryClient, dehydratedState)`, assign the client to the server-rendered element, and only then import the Lit component so it upgrades with the prefetched cache available.