Skip to content

Conversation

@KyleAMathews
Copy link
Collaborator

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)

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

'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

🎯 Changes

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm test:pr.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

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 <[email protected]>
@changeset-bot
Copy link

changeset-bot bot commented Oct 22, 2025

🦋 Changeset detected

Latest commit: 968e37c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@tanstack/react-db Minor
@tanstack/db-example-react-todo Patch

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

@pkg-pr-new
Copy link

pkg-pr-new bot commented Oct 22, 2025

More templates

@tanstack/angular-db

npm i https://pkg.pr.new/@tanstack/angular-db@709

@tanstack/db

npm i https://pkg.pr.new/@tanstack/db@709

@tanstack/db-ivm

npm i https://pkg.pr.new/@tanstack/db-ivm@709

@tanstack/electric-db-collection

npm i https://pkg.pr.new/@tanstack/electric-db-collection@709

@tanstack/query-db-collection

npm i https://pkg.pr.new/@tanstack/query-db-collection@709

@tanstack/react-db

npm i https://pkg.pr.new/@tanstack/react-db@709

@tanstack/rxdb-db-collection

npm i https://pkg.pr.new/@tanstack/rxdb-db-collection@709

@tanstack/solid-db

npm i https://pkg.pr.new/@tanstack/solid-db@709

@tanstack/svelte-db

npm i https://pkg.pr.new/@tanstack/svelte-db@709

@tanstack/trailbase-db-collection

npm i https://pkg.pr.new/@tanstack/trailbase-db-collection@709

@tanstack/vue-db

npm i https://pkg.pr.new/@tanstack/vue-db@709

commit: 968e37c

@github-actions
Copy link
Contributor

github-actions bot commented Oct 22, 2025

Size Change: 0 B

Total Size: 84.3 kB

ℹ️ View Unchanged
Filename Size
./packages/db/dist/esm/collection/change-events.js 1.63 kB
./packages/db/dist/esm/collection/changes.js 1.01 kB
./packages/db/dist/esm/collection/events.js 413 B
./packages/db/dist/esm/collection/index.js 3.23 kB
./packages/db/dist/esm/collection/indexes.js 1.16 kB
./packages/db/dist/esm/collection/lifecycle.js 1.8 kB
./packages/db/dist/esm/collection/mutations.js 2.52 kB
./packages/db/dist/esm/collection/state.js 3.79 kB
./packages/db/dist/esm/collection/subscription.js 2.2 kB
./packages/db/dist/esm/collection/sync.js 2.2 kB
./packages/db/dist/esm/deferred.js 230 B
./packages/db/dist/esm/errors.js 3.48 kB
./packages/db/dist/esm/event-emitter.js 798 B
./packages/db/dist/esm/index.js 1.62 kB
./packages/db/dist/esm/indexes/auto-index.js 794 B
./packages/db/dist/esm/indexes/base-index.js 835 B
./packages/db/dist/esm/indexes/btree-index.js 2 kB
./packages/db/dist/esm/indexes/lazy-index.js 1.21 kB
./packages/db/dist/esm/indexes/reverse-index.js 577 B
./packages/db/dist/esm/local-only.js 967 B
./packages/db/dist/esm/local-storage.js 2.4 kB
./packages/db/dist/esm/optimistic-action.js 294 B
./packages/db/dist/esm/proxy.js 3.86 kB
./packages/db/dist/esm/query/builder/functions.js 615 B
./packages/db/dist/esm/query/builder/index.js 4.04 kB
./packages/db/dist/esm/query/builder/ref-proxy.js 938 B
./packages/db/dist/esm/query/compiler/evaluators.js 1.55 kB
./packages/db/dist/esm/query/compiler/expressions.js 760 B
./packages/db/dist/esm/query/compiler/group-by.js 2.04 kB
./packages/db/dist/esm/query/compiler/index.js 2.21 kB
./packages/db/dist/esm/query/compiler/joins.js 2.65 kB
./packages/db/dist/esm/query/compiler/order-by.js 1.43 kB
./packages/db/dist/esm/query/compiler/select.js 1.28 kB
./packages/db/dist/esm/query/ir.js 785 B
./packages/db/dist/esm/query/live-query-collection.js 404 B
./packages/db/dist/esm/query/live/collection-config-builder.js 5.54 kB
./packages/db/dist/esm/query/live/collection-registry.js 233 B
./packages/db/dist/esm/query/live/collection-subscriber.js 2.11 kB
./packages/db/dist/esm/query/optimizer.js 3.26 kB
./packages/db/dist/esm/scheduler.js 1.29 kB
./packages/db/dist/esm/SortedMap.js 1.24 kB
./packages/db/dist/esm/transactions.js 3.05 kB
./packages/db/dist/esm/utils.js 1.01 kB
./packages/db/dist/esm/utils/browser-polyfills.js 365 B
./packages/db/dist/esm/utils/btree.js 6.01 kB
./packages/db/dist/esm/utils/comparison.js 754 B
./packages/db/dist/esm/utils/index-optimization.js 1.73 kB

compressed-size-action::db-package-size

@github-actions
Copy link
Contributor

github-actions bot commented Oct 22, 2025

Size Change: 0 B

Total Size: 2.89 kB

ℹ️ View Unchanged
Filename Size
./packages/react-db/dist/esm/index.js 168 B
./packages/react-db/dist/esm/useLiveInfiniteQuery.js 1.41 kB
./packages/react-db/dist/esm/useLiveQuery.js 1.31 kB

compressed-size-action::react-db-package-size

claude added 11 commits October 22, 2025 23:04
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 <[email protected]>
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 <[email protected]>
…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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
- 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 <[email protected]>
- 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 <[email protected]>
- 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 <[email protected]>
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 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add SSR & RSC Support for @tanstack/react-db

2 participants