-
Notifications
You must be signed in to change notification settings - Fork 107
Description
Summary
Add server-side rendering (SSR) and React Server Components (RSC) support to @tanstack/react-db following patterns similar to TanStack Query's implementation. This will enable React applications using TanStack DB to efficiently run live queries on the server, serialize the resulting data, and hydrate it on the client for optimal initial page loads.
Background
Currently, @tanstack/react-db provides excellent client-side reactive data management through the useLiveQuery hook, which automatically subscribes to collections and efficiently updates via differential dataflow. However, it lacks built-in SSR/RSC support, which means:
- Live queries must run on the client after initial page load
- No ability to execute queries on the server and transfer results
- Cannot leverage SSR benefits like improved SEO and faster perceived performance
- Missing hydration/dehydration mechanisms for transferring query results to client
- No support for React Server Components data prefetching patterns
TanStack Query has established patterns for SSR with its prefetchQuery, dehydrate, and HydrationBoundary APIs, and has experimental RSC support. We should adapt these patterns for TanStack DB's live query architecture.
Current Architecture
TanStack DB works by:
- Creating collections (local-only, query-backed, or sync-engine backed)
- Running live queries via
useLiveQuerythat automatically subscribe to the underlying collections - Live queries pull data from collections as needed - no explicit collection loading required
- Differential dataflow ensures sub-millisecond updates even for complex queries
Proposed API Design
1. Server-Side Query Execution
// Server-side execution of live queries
import { prefetchLiveQuery, createServerContext } from '@tanstack/react-db/server'
// Create a server context to manage query state
const serverContext = createServerContext({
collections: {
todos: todosCollection,
users: usersCollection,
},
defaultOptions: {
staleTime: 60 * 1000, // Avoid immediate refetch on client
}
})
// Prefetch live query - this loads necessary data from collections
const queryResult = await prefetchLiveQuery(
serverContext,
(q) => q
.from({ todos: todosCollection })
.where(({ todos }) => eq(todos.completed, false))
.select(({ todos }) => ({ id: todos.id, text: todos.text }))
)2. Dehydration
import { dehydrate } from '@tanstack/react-db/server'
// In Next.js getServerSideProps or similar
export async function getServerSideProps() {
const serverContext = createServerContext({
collections: { todos: todosCollection }
})
// Run live queries on server
await prefetchLiveQuery(serverContext, (q) =>
q.from({ todos: todosCollection })
)
return {
props: {
// Dehydrate includes query results and minimal collection data
dehydratedState: dehydrate(serverContext),
},
}
}3. Client-Side Hydration
// HydrationBoundary is a server-only export since it's not needed for client-only apps
import { HydrationBoundary } from '@tanstack/react-db/server'
// In _app.tsx or root component
function MyApp({ Component, pageProps }) {
return (
<HydrationBoundary state={pageProps.dehydratedState}>
<Component {...pageProps} />
</HydrationBoundary>
)
}
// Components use useLiveQuery normally - will use hydrated data if available
function TodoList() {
const { data, isLoading } = useLiveQuery((q) =>
q.from({ todos: todosCollection })
)
// On first render, data is immediately available from SSR
// After hydration, live updates work normally
return <div>{data.map(todo => <Todo key={todo.id} {...todo} />)}</div>
}4. React Server Components Support
For RSC environments (Next.js App Router, etc.):
// In a Server Component
import { prefetchLiveQuery, dehydrate } from '@tanstack/react-db/server'
async function TodoPage() {
const serverContext = createServerContext({
collections: { todos: todosCollection }
})
// Prefetch in Server Component
await prefetchLiveQuery(serverContext, (q) =>
q.from({ todos: todosCollection })
.where(({ todos }) => eq(todos.completed, false))
)
return (
<HydrationBoundary state={dehydrate(serverContext)}>
<TodoList /> {/* Client Component */}
</HydrationBoundary>
)
}
// Client Component uses the data
'use client'
function TodoList() {
const { data } = useLiveQuery((q) =>
q.from({ todos: todosCollection })
.where(({ todos }) => eq(todos.completed, false))
)
return <div>{data.map(todo => <Todo key={todo.id} {...todo} />)}</div>
}React Suspense Integration
Requirements
- Suspense boundaries during hydration phase for smooth loading transitions
- Loading states for live query transitions from server-rendered to client-reactive data
- Streaming SSR compatibility with suspense for progressive content delivery
- Integration with the dehydration/hydration system to handle async data resolution
- Support for concurrent rendering patterns in React 18+
Implementation Considerations
- Suspense fallbacks during client-side query resumption: Handle the transition period when live queries resume reactivity after hydration
- Error boundaries for failed hydration: Graceful handling of hydration mismatches or data corruption
- Concurrent rendering support: Ensure compatibility with React 18's concurrent features and time-slicing
- Streaming integration: Support for React 18 streaming SSR where different components can resolve at different times
API Design
// Suspense-aware live query hook
function TodoList() {
// Suspends during hydration if data isn't immediately available
const { data } = useLiveQuery((q) =>
q.from({ todos: todosCollection })
.where(({ todos }) => eq(todos.completed, false)),
{ suspense: true }
)
// No loading states needed - suspense boundary handles it
return <div>{data.map(todo => <Todo key={todo.id} {...todo} />)}</div>
}
// Usage with Suspense boundary
function App() {
return (
<Suspense fallback={<TodoListSkeleton />}>
<HydrationBoundary state={dehydratedState}>
<TodoList />
</HydrationBoundary>
</Suspense>
)
}Implementation Details
Core Components Needed
-
Server Context
- Lightweight container for managing collections and queries on server
- Tracks which queries were executed
- Collects minimal data needed for hydration
- Ensures isolation between requests
-
Query Prefetching
- Execute live queries on server without differential dataflow overhead
- Extract resulting data and track query configuration
- Handle collection data loading as queries require it
- Support for both sync and async data sources
-
Dehydration System
- Serialize query results and configurations
- Include minimal collection state needed for hydration
- Create compact, transferable representation
- Handle different data types (Dates, Maps, Sets)
-
Hydration System
- Restore query results on client
- Initialize live query collections with server data
- Seamless transition to client-side reactivity
- Prevent unnecessary refetches with stale time
Key Considerations
-
No Global Client
- Unlike TanStack Query, DB doesn't use a global client pattern
- Each query/collection manages its own state
- Server context provides temporary coordination for SSR
-
Collection Data Management
- Only serialize data actually used by queries
- Handle collection initialization on client
- Support different collection types (local, sync-backed)
-
Differential Dataflow
- Server doesn't need full differential dataflow engine
- Client resumes with normal reactive updates after hydration
- Ensure smooth transition without data inconsistencies
-
Query Identity
- Match server queries with client queries
- Handle dynamic queries with different parameters
- Support query composition and joins
Benefits
-
Performance
- Instant data availability on page load
- No loading states for initial render
- Reduced Time to Interactive (TTI)
-
SEO
- Search engines receive fully populated content
- Better indexing of dynamic content
- Improved social media sharing previews
-
Developer Experience
- Minimal API changes -
useLiveQueryworks the same - Familiar patterns for TanStack Query users
- Automatic handling of hydration
- Declarative loading states with Suspense
- Minimal API changes -
-
Efficiency
- Only transfer data actually needed by queries
- Leverage existing collection infrastructure
- No duplicate data fetching
Package Structure
@tanstack/react-db
├── index.ts # Client-only exports (useLiveQuery, etc.)
└── server.ts # Server-only exports (HydrationBoundary, prefetch, dehydrate)
The server exports would only be imported in SSR/RSC contexts, keeping the client bundle lean for SPA applications.
Technical Challenges
-
Query Matching: How to reliably match server-executed queries with client
useLiveQuerycalls? -
Collection State: What minimal collection state needs to be transferred for hydration?
-
Sync Engines: How to handle sync-engine backed collections that may not work on server?
-
Memory Management: Ensure server contexts are properly cleaned up after each request
-
Streaming SSR: Support for React 18 streaming and suspense boundaries
-
RSC Limitations: Live queries are inherently client-side reactive - RSC prefetching is for initial data only
-
Suspense Integration: Ensure smooth transitions between suspended and resolved states during hydration
Prior Art
- TanStack Query SSR: Prefetch/dehydrate/hydrate pattern with experimental RSC support via
@tanstack/react-query-next-experimental - Relay: Query preloading with fragments
- Apollo Client: Cache extraction and restoration
Questions to Address
- Should we support partial hydration for specific queries only?
- How to handle optimistic updates that occur during SSR?
- Should collections themselves be SSR-aware or keep SSR logic separate?
- What's the best way to handle query parameters that change between server and client?
- How to handle the RSC case where Server Components are for prefetching only, not ongoing state management?
- Should we eventually have an experimental package (like
@tanstack/react-db-experimental) for cutting-edge RSC features? - How should suspense boundaries interact with the HydrationBoundary component?
Proposed Implementation Phases
- Phase 1: Server context and basic query prefetching
- Phase 2: Full dehydration/hydration system with server exports
- Phase 3: React Server Components support
- Phase 4: Suspense integration and streaming SSR support
- Phase 5: Advanced features (partial hydration, concurrent rendering optimizations)
- Phase 6: Experimental features package if needed
Related Considerations
- Coordinate with vue-db and angular-db for consistent SSR patterns
- Consider integration with TanStack Start
- Ensure compatibility with all collection types (local, Electric, RxDB, etc.)
References
This feature would complete @tanstack/react-db's capabilities for production SSR applications, providing both powerful client-side reactivity and optimal server-rendering performance.