This document defines the semantics for TanStack Query initialData and placeholderData at the @tanstack/query-db-collection boundary. It is the design follow-up for RFC #1643 and issue #346. The accompanying implementation adds the approved eager initialData behavior without changing the persistence format.
The adapter connects two different models:
initialData and placeholderData must not be treated as equivalent ways to provide an array. In Query Core 5.90.20, initialData initializes Query cache state with status: "success" and a dataUpdatedAt timestamp. By contrast, placeholderData is computed per observer only while Query state is pending; it produces an observer result with isPlaceholderData: true but is not stored in Query state.
Support Query-owned initialData as an additive, eager-mode option. Materialize it immediately through the existing row extraction and ownership pipeline. Do not expose or materialize placeholderData in the first implementation.
Query Collection can already materialize data that was seeded or hydrated into the QueryClient before the observer is created, and QueryClient defaults can already provide initialData indirectly. That is useful existing behavior, but it does not close the configuration gap. Applications commonly share one QueryClient across many Query Collections: a client-wide default is too broad, while imperative setQueryData requires coordinating collection construction, exact Query keys, and initialization elsewhere. The additive field supplies a collection-local declaration while leaving Query as the cache authority.
The minimal additive API is:
initialData?: TQueryData | (() => TQueryData)
initialDataUpdatedAt?: number | (() => number | undefined)These remain flat top-level fields. Their types describe the original Query response, not the extracted row array. Consequently, wrapped responses use the same adapter select as network responses:
queryCollectionOptions({
queryKey: ["todos"],
queryFn: fetchTodos,
initialData: { items: serverTodos, nextCursor: null },
select: (response) => response.items,
// ...
})Query keys still define cache identity. If two Query Collections on the same QueryClient use the same exact key, they observe one shared Query document; initialData initializes that document only when it does not already exist. Collection-local configuration does not create collection-local cache data, and later observers must not replace an existing document with their initializer. Collections that require independent initial documents must use distinct keys.
This initial API is limited to syncMode: "eager". A single configuration-level value cannot lawfully initialize an open-ended family of on-demand subset keys, and Query's initialData function receives no query key or subset context. Applications that already know data for an exact on-demand key should seed or hydrate that Query cache entry instead. A future subset-aware initializer would need an explicit key/subset argument and a separate design.
placeholderData remains Query UI vocabulary. A DB collection has no observer-local result surface: materializing a placeholder would make it visible to every DB query and mutation, assign it row ownership, and potentially persist it. Callers should render placeholders in the consuming UI. A future opt-in temporary-row feature, if needed, should be a DB feature with explicit provenance and lifecycle rather than Query's placeholderData option.
"Authoritative" has two dimensions here. Query owns the authoritative document for a Query key, while DB owns the current normalized row state. A server result is the newest remote snapshot, but local optimistic transactions can temporarily overlay its rows.
| Phase | Query document authority | Materialized row authority | Consequence |
|---|---|---|---|
| No cached data | None | Existing DB/persisted rows, if any | The collection waits for Query; absence is not an empty result. |
| Initial data | Query cache initialData | Its projected rows, subject to normal local overlays | It is a real cached snapshot, not temporary presentation data. |
| Fetch/refetch in flight | Existing Query document | Existing rows | Loading does not clear rows or ownership. |
| Server success | Returned response replaces the Query document | Projected server rows reconcile the owning Query key | Missing rows lose this Query owner's lease; shared rows remain. |
| Fetch/refetch error | Last successful Query document | Existing rows | An error does not retract initial or previously fetched rows. |
| Placeholder presentation | No Query document | No rows | Placeholder data is never passed to DB. |
initialDataUpdatedAt and staleTime remain Query-owned. They decide whether a fetch starts; the adapter does not reproduce their freshness calculation. An initial value is therefore a seed snapshot with ordinary Query authority, not a weaker class of row waiting to be promoted. The first successful server result reconciles it through the same path as any later refetch.
| Concern | initialData | placeholderData |
|---|---|---|
| Query cache | Stored as successful Query data | Not stored; observer-only |
| Existing cache entry | Existing cached/hydrated data wins; initialData is not reapplied | Not applicable |
| DB materialization | Immediate in eager mode | Never |
| Wrapped response | Adapter select(initialData) extracts rows; original envelope stays cached | Unsupported |
| Function value | Evaluated by Query once when the Query is created | Not forwarded or evaluated by the adapter |
| Ownership | The Query key owns projected rows exactly like a server success | No ownership |
| Overlapping subsets | Not applicable to the initial eager-only API | Not applicable |
| Ready state | Initial successful result can make the collection ready synchronously | Cannot make the collection ready |
| Refetch success | Reconciles additions, updates, and removals normally | N/A |
| Refetch error | Initial rows and ownership remain; error state is reported | N/A |
| Cancellation/cleanup | Existing ownership cleanup rules apply; a cancelled fetch does not retract the cached seed | No rows to clean up |
| Query cache GC/unload | Existing Query-to-row ownership and persisted-retention rules apply | No effect |
| Query dehydration | Query owns persistence of the initial response | Never persisted |
| DB persistence/hydration | Rows and owner metadata use the existing format; no provenance tag is added | Never persisted or hydrated |
| Direct writes before server success | Allowed under the existing write rules below | No target rows exist |
| QueryClient defaults | Supported for eager initialData; see compatibility guard below | Must be suppressed at this adapter boundary |
Adapter select remains a one-way row extractor. It is applied identically to initial and network responses, and TanStack Query retains the original response shape. Query observer-level select remains unsupported.
Direct writes before the first server result follow the current authority rule: they update DB immediately and may patch Query cache only when the reverse update is lawful. A raw array can be replaced. For a wrapped response, the existing best-effort patch is lawful only when select returns an array property of the cached object by reference, allowing the wrapper to be preserved. A derived projection such as response.edges.map(...) has no general reverse projection; the adapter must leave that Query document unchanged and rely on invalidate or refetch. It must never fabricate an envelope around rows.
The next successful remote response remains authoritative for that Query key and may overwrite a direct cache patch or normalized row value. Mutation handlers and optimistic transaction barriers retain their existing semantics.
Initial rows use the existing queryToRows and rowToQueries relationship. No seed, temporary, or placeholder bit is added to a row. This keeps these invariants intact:
The expected transitions are:
Query Collection currently constructs a QueryObserver, so QueryClient defaults can contain semantic fields even when Query Collection does not expose them. Implementation must explicitly enforce this design after Query defaults are resolved:
Silently materializing default placeholder data would violate the public compatibility table even before a top-level field is added. The guard is therefore a correctness fix, not support for placeholder semantics.
Other Query-owned options keep their current classification. Query key creation, adapter select, subscriptions, notifyOnChangeProps, and structural sharing remain adapter-owned or reinterpreted. No nested queryOptions, runtime binding API, subset deduplication, or lease manager is introduced by this design.
Each behavior PR should begin with the named failing or characterization tests.
Placeholder materialization, subset-aware initialization, and a lawful general reverse projection are separate future proposals. None is a prerequisite for the minimal eager initialData API.