diff --git a/.changeset/stable-viewkeys-for-temp-ids.md b/.changeset/stable-viewkeys-for-temp-ids.md new file mode 100644 index 000000000..f6e7bd410 --- /dev/null +++ b/.changeset/stable-viewkeys-for-temp-ids.md @@ -0,0 +1,31 @@ +--- +"@tanstack/db": patch +--- + +Add stable `viewKey` support to prevent UI re-renders during temporary-to-real ID transitions. When inserting items with temporary IDs that are later replaced by server-generated IDs, React components would previously unmount and remount, causing loss of focus and visual flicker. + +Collections can now be configured with a `viewKey` function to generate stable keys: + +```typescript +const todoCollection = createCollection({ + getKey: (item) => item.id, + viewKey: () => crypto.randomUUID(), + onInsert: async ({ transaction }) => { + const tempId = transaction.mutations[0].modified.id + const response = await api.create(...) + + // Link temporary and real IDs to same viewKey + todoCollection.mapViewKey(tempId, response.id) + await todoCollection.utils.refetch() + }, +}) + +// Use stable keys in React +
  • +``` + +New APIs: + +- `collection.getViewKey(key)` - Returns stable viewKey for any key (temporary or real) +- `collection.mapViewKey(tempKey, realKey)` - Links temporary and real IDs to share the same viewKey +- `viewKey` configuration option - Function to generate stable view keys for inserted items diff --git a/docs/guides/mutations.md b/docs/guides/mutations.md index 24e8aba92..3414695bb 100644 --- a/docs/guides/mutations.md +++ b/docs/guides/mutations.md @@ -1127,30 +1127,17 @@ const TodoItem = ({ todo, isPersisted }: { todo: Todo, isPersisted: boolean }) = } ``` -### Solution 3: Maintain a View Key Mapping +### Solution 3: Use Built-in Stable View Keys -To avoid UI flicker while keeping optimistic updates, maintain a separate mapping from IDs (both temporary and real) to stable view keys: +TanStack DB provides built-in support for stable view keys that prevent UI flicker during ID transitions: ```tsx -// Create a mapping API -const idToViewKey = new Map() - -function getViewKey(id: number | string): string { - if (!idToViewKey.has(id)) { - idToViewKey.set(id, crypto.randomUUID()) - } - return idToViewKey.get(id)! -} - -function linkIds(tempId: number, realId: number) { - const viewKey = getViewKey(tempId) - idToViewKey.set(realId, viewKey) -} - -// Configure collection to link IDs when real ID comes back +// Configure collection with automatic view key generation const todoCollection = createCollection({ id: "todos", - // ... other options + getKey: (item) => item.id, + // Enable automatic view key generation + viewKey: () => crypto.randomUUID(), onInsert: async ({ transaction }) => { const mutation = transaction.mutations[0] const tempId = mutation.modified.id @@ -1162,17 +1149,16 @@ const todoCollection = createCollection({ }) const realId = response.id - // Link temp ID to same view key as real ID - linkIds(tempId, realId) + // Link temp ID to real ID (they share the same viewKey) + todoCollection.mapViewKey(tempId, realId) // Wait for sync back await todoCollection.utils.refetch() }, }) -// When inserting with temp ID +// Insert with temp ID - viewKey is automatically generated const tempId = -Math.floor(Math.random() * 1000000) + 1 -const viewKey = getViewKey(tempId) // Creates and stores mapping todoCollection.insert({ id: tempId, @@ -1180,7 +1166,7 @@ todoCollection.insert({ completed: false }) -// Use view key for rendering +// Use getViewKey() for stable rendering keys const TodoList = () => { const { data: todos } = useLiveQuery((q) => q.from({ todo: todoCollection }) @@ -1189,7 +1175,7 @@ const TodoList = () => { return (