Skip to content

Commit 98bb88b

Browse files
committed
feat: add runtime deprecation warnings for handler return values
Based on external code review feedback, this commit implements a soft deprecation strategy for mutation handler return values: Runtime changes: - Add console warnings when QueryCollection handlers return { refetch } - Add console warnings when Electric handlers return { txid } - Keep runtime functionality intact for backward compatibility - Runtime support will be removed in v1.0 RC Type improvements: - Mark TReturn generic as @internal and deprecated across all mutation types - Clarify that it exists only for backward compatibility Documentation improvements: - Clarify Electric JSDoc: handlers return Promise<void> but must not RESOLVE until synchronization is complete (avoiding "void but not void" confusion) - Add timeout error handling example showing policy choices (rollback vs eventual consistency) - Update changeset to clearly communicate this is a soft deprecation This aligns with the review recommendation for a gradual migration path with clear runtime feedback to help users migrate to the new explicit patterns.
1 parent b44463f commit 98bb88b

File tree

4 files changed

+102
-17
lines changed

4 files changed

+102
-17
lines changed

.changeset/deprecate-handler-return-values.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,21 @@
44
"@tanstack/query-db-collection": major
55
---
66

7-
**BREAKING**: Deprecate returning values from mutation handlers (`onInsert`, `onUpdate`, `onDelete`). Instead, use explicit sync coordination:
7+
**BREAKING (TypeScript only)**: Deprecate returning values from mutation handlers (`onInsert`, `onUpdate`, `onDelete`).
88

9+
**What's changed:**
10+
- Handler types now default to `Promise<void>` instead of `Promise<any>`
11+
- TypeScript will error on `return { refetch: false }` or `return { txid }`
12+
- Runtime still supports old return patterns for backward compatibility
13+
- **Deprecation warnings** are now logged when handlers return values
14+
- Old patterns will be fully removed in v1.0 RC
15+
16+
**New pattern (explicit sync coordination):**
917
- **Query Collections**: Call `await collection.utils.refetch()` to sync server state
1018
- **Electric Collections**: Call `await collection.utils.awaitTxId(txid)` or `await collection.utils.awaitMatch(fn)` to wait for synchronization
1119
- **Other Collections**: Use appropriate sync utilities for your collection type
1220

13-
This change makes the API more explicit and consistent across all collection types. Magic return values like `{ refetch: false }` in Query Collections and `{ txid }` in Electric Collections are now deprecated. All handlers should coordinate sync explicitly within the handler function.
21+
This change makes the API more explicit and consistent across all collection types. All handlers should coordinate sync explicitly within the handler function using `await`, rather than relying on magic return values.
1422

1523
Migration guide:
1624

packages/db/src/types.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,20 +363,29 @@ export type DeleteMutationFnParams<
363363
collection: Collection<T, TKey, TUtils>
364364
}
365365

366+
/**
367+
* @typeParam TReturn - @internal DEPRECATED: Defaults to void. Only kept for backward compatibility. Will be removed in v1.0.
368+
*/
366369
export type InsertMutationFn<
367370
T extends object = Record<string, unknown>,
368371
TKey extends string | number = string | number,
369372
TUtils extends UtilsRecord = UtilsRecord,
370373
TReturn = void,
371374
> = (params: InsertMutationFnParams<T, TKey, TUtils>) => Promise<TReturn>
372375

376+
/**
377+
* @typeParam TReturn - @internal DEPRECATED: Defaults to void. Only kept for backward compatibility. Will be removed in v1.0.
378+
*/
373379
export type UpdateMutationFn<
374380
T extends object = Record<string, unknown>,
375381
TKey extends string | number = string | number,
376382
TUtils extends UtilsRecord = UtilsRecord,
377383
TReturn = void,
378384
> = (params: UpdateMutationFnParams<T, TKey, TUtils>) => Promise<TReturn>
379385

386+
/**
387+
* @typeParam TReturn - @internal DEPRECATED: Defaults to void. Only kept for backward compatibility. Will be removed in v1.0.
388+
*/
380389
export type DeleteMutationFn<
381390
T extends object = Record<string, unknown>,
382391
TKey extends string | number = string | number,
@@ -413,6 +422,11 @@ export type CollectionStatus =
413422

414423
export type SyncMode = `eager` | `on-demand`
415424

425+
/**
426+
* @typeParam TReturn - @internal DEPRECATED: This generic parameter exists for backward compatibility only.
427+
* Mutation handlers should not return values. Use collection utilities (refetch, awaitTxId, etc.) for sync coordination.
428+
* This parameter will be removed in v1.0.
429+
*/
416430
export interface BaseCollectionConfig<
417431
T extends object = Record<string, unknown>,
418432
TKey extends string | number = string | number,

packages/electric-db-collection/src/electric.ts

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -122,13 +122,15 @@ export interface ElectricCollectionConfig<
122122
* Optional asynchronous handler function called before an insert operation
123123
*
124124
* **IMPORTANT - Electric Synchronization:**
125-
* Electric collections require explicit synchronization coordination to ensure changes have synced
126-
* from the server before dropping optimistic state. Use one of these patterns:
127-
* 1. Call `await collection.utils.awaitTxId(txid)` (recommended for most cases)
128-
* 2. Call `await collection.utils.awaitMatch()` for custom matching logic
125+
* This handler returns `Promise<void>`, but **must not resolve** until synchronization is confirmed.
126+
* You must await one of these synchronization utilities before the handler completes:
127+
* 1. `await collection.utils.awaitTxId(txid)` (recommended for most cases)
128+
* 2. `await collection.utils.awaitMatch(fn)` for custom matching logic
129+
*
130+
* Simply returning without waiting for sync will drop optimistic state too early, causing UI glitches.
129131
*
130132
* @param params Object containing transaction and collection information
131-
* @returns Promise that should resolve to void
133+
* @returns Promise<void> - Must not resolve until synchronization is complete
132134
* @deprecated Returning { txid } from handlers is deprecated. Use `await collection.utils.awaitTxId(txid)` instead.
133135
*
134136
* @example
@@ -154,6 +156,26 @@ export interface ElectricCollectionConfig<
154156
* }
155157
*
156158
* @example
159+
* // Insert handler with timeout error handling
160+
* onInsert: async ({ transaction, collection }) => {
161+
* const newItem = transaction.mutations[0].modified
162+
* const result = await api.todos.create({
163+
* data: newItem
164+
* })
165+
*
166+
* try {
167+
* await collection.utils.awaitTxId(result.txid, 5000)
168+
* } catch (error) {
169+
* // Decide sync timeout policy:
170+
* // - Throw to rollback optimistic state
171+
* // - Catch to keep optimistic state (eventual consistency)
172+
* // - Schedule background retry
173+
* console.warn('Sync timeout, keeping optimistic state:', error)
174+
* // Don't throw - allow optimistic state to persist
175+
* }
176+
* }
177+
*
178+
* @example
157179
* // Insert handler with multiple items
158180
* onInsert: async ({ transaction, collection }) => {
159181
* const items = transaction.mutations.map(m => m.modified)
@@ -185,13 +207,15 @@ export interface ElectricCollectionConfig<
185207
* Optional asynchronous handler function called before an update operation
186208
*
187209
* **IMPORTANT - Electric Synchronization:**
188-
* Electric collections require explicit synchronization coordination to ensure changes have synced
189-
* from the server before dropping optimistic state. Use one of these patterns:
190-
* 1. Call `await collection.utils.awaitTxId(txid)` (recommended for most cases)
191-
* 2. Call `await collection.utils.awaitMatch()` for custom matching logic
210+
* This handler returns `Promise<void>`, but **must not resolve** until synchronization is confirmed.
211+
* You must await one of these synchronization utilities before the handler completes:
212+
* 1. `await collection.utils.awaitTxId(txid)` (recommended for most cases)
213+
* 2. `await collection.utils.awaitMatch(fn)` for custom matching logic
214+
*
215+
* Simply returning without waiting for sync will drop optimistic state too early, causing UI glitches.
192216
*
193217
* @param params Object containing transaction and collection information
194-
* @returns Promise that should resolve to void
218+
* @returns Promise<void> - Must not resolve until synchronization is complete
195219
* @deprecated Returning { txid } from handlers is deprecated. Use `await collection.utils.awaitTxId(txid)` instead.
196220
*
197221
* @example
@@ -225,13 +249,15 @@ export interface ElectricCollectionConfig<
225249
* Optional asynchronous handler function called before a delete operation
226250
*
227251
* **IMPORTANT - Electric Synchronization:**
228-
* Electric collections require explicit synchronization coordination to ensure changes have synced
229-
* from the server before dropping optimistic state. Use one of these patterns:
230-
* 1. Call `await collection.utils.awaitTxId(txid)` (recommended for most cases)
231-
* 2. Call `await collection.utils.awaitMatch()` for custom matching logic
252+
* This handler returns `Promise<void>`, but **must not resolve** until synchronization is confirmed.
253+
* You must await one of these synchronization utilities before the handler completes:
254+
* 1. `await collection.utils.awaitTxId(txid)` (recommended for most cases)
255+
* 2. `await collection.utils.awaitMatch(fn)` for custom matching logic
256+
*
257+
* Simply returning without waiting for sync will drop optimistic state too early, causing UI glitches.
232258
*
233259
* @param params Object containing transaction and collection information
234-
* @returns Promise that should resolve to void
260+
* @returns Promise<void> - Must not resolve until synchronization is complete
235261
* @deprecated Returning { txid } from handlers is deprecated. Use `await collection.utils.awaitTxId(txid)` instead.
236262
*
237263
* @example
@@ -584,6 +610,13 @@ export function electricCollectionOptions(
584610
): Promise<void> => {
585611
// Only wait if result contains txid
586612
if (result && `txid` in result) {
613+
// Warn about deprecated return value pattern
614+
console.warn(
615+
'[TanStack DB] DEPRECATED: Returning { txid } from mutation handlers is deprecated and will be removed in v1.0. ' +
616+
'Use `await collection.utils.awaitTxId(txid)` instead of returning { txid }. ' +
617+
'See migration guide: https://tanstack.com/db/latest/docs/collections/electric-collection#persistence-handlers--synchronization'
618+
)
619+
587620
const timeout = result.timeout
588621
// Handle both single txid and array of txids
589622
if (Array.isArray(result.txid)) {

packages/query-db-collection/src/query.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,6 +1033,16 @@ export function queryCollectionOptions(
10331033
const wrappedOnInsert = onInsert
10341034
? async (params: InsertMutationFnParams<any>) => {
10351035
const handlerResult = (await onInsert(params)) ?? {}
1036+
1037+
// Warn about deprecated return value pattern
1038+
if (handlerResult && typeof handlerResult === 'object' && Object.keys(handlerResult).length > 0) {
1039+
console.warn(
1040+
'[TanStack DB] DEPRECATED: Returning values from mutation handlers is deprecated and will be removed in v1.0. ' +
1041+
'Use `await collection.utils.refetch()` instead of returning { refetch }. ' +
1042+
'See migration guide: https://tanstack.com/db/latest/docs/guides/mutations#collection-specific-handler-patterns'
1043+
)
1044+
}
1045+
10361046
const shouldRefetch =
10371047
(handlerResult as { refetch?: boolean }).refetch !== false
10381048

@@ -1047,6 +1057,16 @@ export function queryCollectionOptions(
10471057
const wrappedOnUpdate = onUpdate
10481058
? async (params: UpdateMutationFnParams<any>) => {
10491059
const handlerResult = (await onUpdate(params)) ?? {}
1060+
1061+
// Warn about deprecated return value pattern
1062+
if (handlerResult && typeof handlerResult === 'object' && Object.keys(handlerResult).length > 0) {
1063+
console.warn(
1064+
'[TanStack DB] DEPRECATED: Returning values from mutation handlers is deprecated and will be removed in v1.0. ' +
1065+
'Use `await collection.utils.refetch()` instead of returning { refetch }. ' +
1066+
'See migration guide: https://tanstack.com/db/latest/docs/guides/mutations#collection-specific-handler-patterns'
1067+
)
1068+
}
1069+
10501070
const shouldRefetch =
10511071
(handlerResult as { refetch?: boolean }).refetch !== false
10521072

@@ -1061,6 +1081,16 @@ export function queryCollectionOptions(
10611081
const wrappedOnDelete = onDelete
10621082
? async (params: DeleteMutationFnParams<any>) => {
10631083
const handlerResult = (await onDelete(params)) ?? {}
1084+
1085+
// Warn about deprecated return value pattern
1086+
if (handlerResult && typeof handlerResult === 'object' && Object.keys(handlerResult).length > 0) {
1087+
console.warn(
1088+
'[TanStack DB] DEPRECATED: Returning values from mutation handlers is deprecated and will be removed in v1.0. ' +
1089+
'Use `await collection.utils.refetch()` instead of returning { refetch }. ' +
1090+
'See migration guide: https://tanstack.com/db/latest/docs/guides/mutations#collection-specific-handler-patterns'
1091+
)
1092+
}
1093+
10641094
const shouldRefetch =
10651095
(handlerResult as { refetch?: boolean }).refetch !== false
10661096

0 commit comments

Comments
 (0)