React hooks for optimistic updates
npm install @tanstack/react-optimisticReact hooks and utilities for creating fast optimistic updates with flexible backend support that pairs seamlessly with sync engines (like ElectricSQL).
``bash`
pnpm add @TanStack/react-optimistic
@TanStack/react-optimistic provides React-specific hooks and utilities for managing data synchronization between your frontend application and backend services. It offers:
- Optimistic Updates: Apply changes instantly in the UI while syncing in the background
- Flexible Backend Support: Works with any backend or sync engine
- Immutable Snapshots: Create immutable snapshots of updates that can be persisted and rolled back
- React Integration: Seamless integration with React components and state management
The primary hook for interacting with collections in React components.
`typescript`
const { data, insert, update, delete: deleteFn } = useCollection({
id: 'todos',
sync: { / sync configuration / },
mutationFn: { / mutation functions / },
schema: / optional schema /
});
Returns:
- data: An array of all items in the collectionstate
- : A Map containing all items in the collection with their internal keysinsert
- : Function to add new items to the collectionupdate
- : Function to modify existing itemsdelete
- : Function to remove items from the collection
Preloads data for a collection before rendering components.
`typescript`
await preloadCollection({
id: 'todos',
sync: { / sync configuration / },
mutationFn: { / mutation functions / },
schema: / optional schema /
});
Features:
1. Returns a promise that resolves when the first sync commit is complete
2. Shares the same collection instance with useCollection
3. Handles already-loaded collections by returning immediately
4. Avoids duplicate initialization when called multiple times with the same ID
`typescript
// Insert a single item
insert({ text: "Buy groceries", completed: false })
// Insert multiple items
insert([
{ text: "Buy groceries", completed: false },
{ text: "Walk dog", completed: false },
])
// Insert with custom key
insert({ text: "Buy groceries" }, { key: "grocery-task" })
`
We use a proxy to capture updates as immutable draft optimistic updates.
`typescript
// Update a single item
update(todo, (draft) => {
draft.completed = true
})
// Update multiple items
update([todo1, todo2], (drafts) => {
drafts.forEach((draft) => {
draft.completed = true
})
})
// Update with metadata
update(todo, { metadata: { reason: "user update" } }, (draft) => {
draft.text = "Updated text"
})
`
`typescript
// Delete a single item
delete todo
// Delete multiple items
delete [todo1, todo2]
// Delete with metadata
delete (todo, { metadata: { reason: "completed" } })
`
The mutationFn property is where you define how your application interacts with your backend. Here's a comprehensive example of integrating with ElectricSQL:
`typescript
import { useCollection } from "@TanStack/react-optimistic"
import { createElectricSync } from "@TanStack/optimistic/electric"
// Create a collection configuration for todos
const todosConfig = {
id: "todos",
// Create an ElectricSQL sync configuration
sync: createElectricSync(
{
// ShapeStream options
url: http://localhost:3000/v1/shape,
params: {
table: "todos",
},
},
{
// Primary key for the todos table
primaryKey: ['id'],
}
),
}
// Use the collection in a component
function TodoList() {
const { data, insert, update, delete: deleteFn } = useCollection(todosConfig)
// Create a mutation for handling all todo operations
const todoMutation = useOptimisticMutation({
mutationFn: async ({ transaction }) => {
const payload = transaction.mutations.map(m => {
const { collection, ...payload } = m
return payload
})
const response = await fetch(http://localhost:3001/api/mutations, {POST
method: ,application/json
headers: {
"Content-Type": ,
},
body: JSON.stringify(payload),
})
if (!response.ok) {
throw new Error(HTTP error! Status: ${response.status})
}
const result = await response.json()
await transaction.mutations[0].collection.config.sync.awaitTxid(result.txid)
}
})
const addTodo = () => {
todoMutation.mutate(() => {
insert({ title: 'New todo', completed: false })
})
}
const toggleTodo = (todo) => {
todoMutation.mutate(() => {
update(todo, (draft) => {
draft.completed = !draft.completed
})
})
}
const removeTodo = (todo) => {
todoMutation.mutate(() => {
deleteFn(todo)
})
}
return (