-
Couldn't load subscription status.
- Fork 107
feat: Add SSR/RSC support for live queries in @tanstack/react-db #709
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: Add SSR/RSC support for live queries in @tanstack/react-db #709
Conversation
Implements SSR (Server-Side Rendering) and RSC (React Server Components) support for live queries, following TanStack Query's hydration patterns.
## Key Features
- **Server-side query execution**: `prefetchLiveQuery()` executes queries on the server and extracts results
- **Dehydration**: `dehydrate()` serializes query results for client transfer
- **Hydration**: Automatic hydration of query results on the client via React Context or global state
- **HydrationBoundary**: Component to provide hydrated data to child components
- **Seamless integration**: `useLiveQuery` automatically uses hydrated data when available
## Implementation Details
- Creates temporary collections on server for query execution
- Stores only query results (not full collection state) for minimal payload
- Client checks for hydrated data when collection is empty
- Smooth transition from hydrated data to live reactive updates
- Supports both HydrationBoundary context and direct `hydrate()` calls
## API Usage
### Server-side (Next.js App Router example)
```tsx
async function Page() {
const serverContext = createServerContext()
await prefetchLiveQuery(serverContext, {
id: 'todos',
query: (q) => q.from({ todos: todosCollection })
})
return (
<HydrationBoundary state={dehydrate(serverContext)}>
<TodoList />
</HydrationBoundary>
)
}
```
### Client-side
```tsx
'use client'
function TodoList() {
const { data } = useLiveQuery({
id: 'todos',
query: (q) => q.from({ todos: todosCollection })
})
return <div>{data.map(todo => <Todo key={todo.id} {...todo} />)}</div>
}
```
## Testing
- Added comprehensive test suite for SSR/RSC functionality
- Tests cover prefetching, dehydration, hydration, and HydrationBoundary
- All existing tests pass (67 total tests)
- Code coverage improved from 75.77% to 86.5%
Closes #545
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 968e37c The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
More templates
@tanstack/angular-db
@tanstack/db
@tanstack/db-ivm
@tanstack/electric-db-collection
@tanstack/query-db-collection
@tanstack/react-db
@tanstack/rxdb-db-collection
@tanstack/solid-db
@tanstack/svelte-db
@tanstack/trailbase-db-collection
@tanstack/vue-db
commit: |
|
Size Change: 0 B Total Size: 84.3 kB ℹ️ View Unchanged
|
|
Size Change: 0 B Total Size: 2.89 kB ℹ️ View Unchanged
|
Demonstrates SSR/RSC support for live queries in the TanStack Start projects example app. ## Changes **Project Detail Page** (`src/routes/_authenticated/project/$projectId.tsx`): - ✅ Enabled SSR (`ssr: true`) - ✅ Added server-side prefetching in loader using `createServerContext` and `prefetchLiveQuery` - ✅ Wrapped component with `HydrationBoundary` to provide server-rendered data - ✅ Updated all `useLiveQuery` calls to include `id` option for hydration matching - ✅ Added comprehensive inline comments explaining the SSR/RSC pattern **Prefetched Queries:** - Project details - Project todos (filtered and ordered) - All users (for member management) - Project membership info **README Updates:** - Added new "Server-Side Rendering (SSR) & React Server Components (RSC)" section - Included complete code example showing the pattern - Documented benefits (instant rendering, SEO, performance) - Explained key concepts (query ID matching, minimal payload, automatic transitions) ## Benefits - **Zero loading states**: Page renders immediately with server data - **SEO optimized**: Fully populated HTML for search engines - **Smooth UX**: Seamless transition from server data to live reactive updates - **Minimal payload**: Only query results transferred, not full collection state ## Usage The implementation follows TanStack Query's hydration patterns: 1. Server prefetches queries and dehydrates results 2. HydrationBoundary provides data to components 3. useLiveQuery with matching IDs uses hydrated data 4. Queries automatically become reactive after hydration This serves as a reference implementation for SSR/RSC with TanStack DB live queries. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit addresses priority fixes from external code review:
- Fix singleResult bug in hydrated data path - now properly returns single object vs array
- Remove unused staleTime option from prefetchLiveQuery
- Split server.tsx into server.ts (server-only) and hydration.tsx ('use client') for proper RSC module boundaries
- Add test coverage for singleResult with hydrated data
- Improve option detection from fragile duck typing to explicit property check
- Add comprehensive serialization constraints documentation to README
All 68 tests passing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
…ration, and enhanced testing This commit addresses the second round of code review feedback: **Critical fixes:** - Fix README docs bug - async query example now correctly uses transform callback instead of async function - Switch to toArrayWhenReady() in prefetchLiveQuery for rock-solid first render guarantees **New features:** - Add transform option to prefetchLiveQuery for server-side data transformations (e.g., Date serialization) - Implement safer global hydration using Symbol.for() to avoid bundle collisions - Add oneShot option to hydrate() for one-time consumption patterns **Enhanced testing:** - Add nested HydrationBoundary test (inner shadows outer) - Add one-shot hydration test - Update existing test to check new Symbol-based storage structure **Developer experience improvements:** - Comprehensive JSDoc examples for transform option - Clearer documentation for serialization constraints - More robust global state handling All 70 tests passing with 89.94% coverage. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit addresses the final round of code review feedback to make the SSR/RSC implementation production-ready: **Critical bug fixes:** - Fix test cleanup to clear Symbol-based global state, preventing test flakiness - Align status reporting in hydrated path with underlying collection status for consistency - Users can now trust `status` and `isReady` flags even during hydration **Type safety & robustness:** - Harden transform types to accept `Array<any> | any` return values - Add runtime normalization to ensure transform output is always an array - Updated docs: "transform should return an array of rows; non-arrays are normalized to a single-element array" **Public API hygiene:** - Remove internal `useHydratedQuery` from root exports to prevent accidental coupling - Explicitly export only public APIs: createServerContext, prefetchLiveQuery, dehydrate, HydrationBoundary, hydrate - Keep types exported for advanced use cases **Developer experience improvements:** - Add subpath exports in package.json for explicit server/client boundaries: - `@tanstack/react-db/server` for server-only code - `@tanstack/react-db/hydration` for client components - Document subpath imports in README with examples - Add oneShot option documentation to API reference - Mention transform option in API reference **Benefits:** - Better bundler optimization for RSC environments - Clearer intent in imports (server vs. client) - Reduced bundle size by preventing internal API leakage - More predictable behavior during hydration transitions All 70 tests passing with 90% coverage. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This review document provides: - Complete API comparison (requested vs implemented) - Technical challenges and solutions - Phase 1-3 completion status - Phase 4-6 deferral explanation - Test coverage summary - Bonus features added during code review Useful for PR description and future reference. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Based on user feedback, we're removing the global hydrate() function and oneShot option
in favor of exclusively using HydrationBoundary for a simpler, more React-idiomatic API.
**Changes:**
- Remove hydrate() function and oneShot option from hydration.tsx
- Remove Symbol-based global storage (no longer needed)
- Remove getHydratedQuery() helper function
- Update useLiveQuery to only check HydrationContext (no global fallback)
- Update all tests to use HydrationBoundary wrapper instead of global hydrate()
- Remove "should hydrate state globally" test (no longer applicable)
- Remove "should support one-shot hydration" test (feature removed)
- Update README to remove hydrate() from API reference
- Update index.ts to not export hydrate()
**Benefits:**
- Simpler API surface (one way to do hydration)
- No global state pollution
- No memory management complexity
- React manages lifecycle automatically via Context
- More predictable behavior (no hidden globals)
**Migration:**
Before:
```tsx
hydrate(dehydratedState, { oneShot: true })
```
After:
```tsx
<HydrationBoundary state={dehydratedState}>
{children}
</HydrationBoundary>
```
All 68 tests passing with 89.61% coverage.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit addresses final documentation cleanup to ensure the public API is unambiguous:
**Documentation cleanup:**
- Remove hydrate() from changeset API list
- Remove "direct hydrate() approaches" mention from changeset
- Add transform option to changeset API docs
- Update IMPLEMENTATION_REVIEW.md to remove all global hydrate() references
- Remove OneShot and Symbol storage sections (features removed)
- Update section numbering after removal
- Update "Safety" line to remove Symbol globals mention
**Dev experience improvement:**
- Add console.warn in development when useLiveQuery has an id but no hydrated data found
- Warning message: "TanStack DB: no hydrated data found for id "..." — did you wrap this subtree in <HydrationBoundary state={...}>?"
- Only fires when:
- process.env.NODE_ENV !== 'production'
- queryId is provided
- Collection is empty
- No hydrated data found in context
This helps developers quickly identify SSR hydration setup issues.
**Transform type signature verified:**
- Already correct: `transform?: (rows: Array<any>) => Array<any> | any`
- Defensive normalization already in place: `Array.isArray(out) ? out : [out]`
All 68 tests passing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Format documentation files with prettier - Improve clarity in changeset and implementation review - Ensure all references to removed hydrate() function are cleaned up - Polish README examples and API reference sections 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
- Update server.ts to use hydrateId in PrefetchLiveQueryOptions - Update hydration.tsx to match queries by hydrateId - Update useLiveQuery.ts to extract hydrateId from config - Separates SSR hydration identifier from collection id (used for devtools) This allows users to freely use id for devtools/debugging without triggering SSR hydration warnings. Only hydrateId opts into SSR. WIP: Tests and documentation still need updating. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
- Extract hydrateId only from config objects (not collections/functions)
- Update dev warning to reference hydrateId instead of id
- Aligns with server API pattern: prefetchLiveQuery({ hydrateId, query })
This ensures consistency between server and client APIs.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Changed from hydrateId back to id to align with TanStack Query patterns. Updated dev warning to only trigger when: - HydrationBoundary context exists (SSR environment detected) - Query has an id - No matching hydrated data found This prevents false warnings in client-only apps while still catching SSR setup mistakes. Benefits: - Simpler API (single identifier like TanStack Query) - No warnings in client-only apps (no hydration context = no warning) - Helpful warnings in SSR apps when query wasn't prefetched - id serves dual purpose: collection identity + SSR matching All 11 SSR tests passing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Implements SSR (Server-Side Rendering) and RSC (React Server Components) support for live queries, following TanStack Query's hydration patterns.
Key Features
prefetchLiveQuery()executes queries on the server and extracts resultsdehydrate()serializes query results for client transferuseLiveQueryautomatically uses hydrated data when availableImplementation Details
hydrate()callsAPI Usage
Server-side (Next.js App Router example)
Client-side
Testing
Closes #545
🤖 Generated with Claude Code
🎯 Changes
✅ Checklist
pnpm test:pr.🚀 Release Impact