Skip to content

Commit b42479c

Browse files
Make transactions first class & move ownership of mutationFn from collections to transactions (#53)
Co-authored-by: Sam Willis <sam.willis@gmail.com>
1 parent f979ffd commit b42479c

32 files changed

+1473
-1248
lines changed

.changeset/sweet-kings-wear.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@tanstack/react-optimistic": patch
3+
"@tanstack/optimistic": patch
4+
---
5+
6+
Make transactions first class & move ownership of mutationFn from collections to transactions

README.md

Lines changed: 77 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,25 @@ The library uses proxies to create immutable snapshots and track changes:
5858
The primary hook for interacting with collections in React components.
5959

6060
```typescript
61+
// Create a collection
6162
const { data, insert, update, delete: deleteFn } = useCollection({
6263
id: 'todos',
6364
sync: { /* sync configuration */ },
64-
mutationFn: { /* mutation functions */ },
6565
schema: /* optional schema */
6666
});
67+
68+
// Create a mutation
69+
const mutation = useOptimisticMutation({
70+
mutationFn: async ({ mutations }) => {
71+
// Implement your mutation logic here
72+
// This function is called when mutations are committed
73+
}
74+
});
75+
76+
// Use the mutation with collection operations
77+
mutation.mutate(() => {
78+
insert({ text: 'New todo' });
79+
});
6780
```
6881

6982
Returns:
@@ -167,10 +180,22 @@ const todoCollection = useCollection({
167180

168181
## Transaction Management
169182

170-
The library includes a robust transaction management system:
183+
The library includes a simple yet powerful transaction management system. Transactions are created using the `createTransaction` function:
184+
185+
```typescript
186+
const tx = createTransaction({
187+
mutationFn: async ({ transaction }) => {
188+
// Implement your mutation logic here
189+
// This function is called when the transaction is committed
190+
},
191+
})
171192

172-
- `TransactionManager`: Handles transaction lifecycle, persistence, and retry logic
173-
- `TransactionStore`: Provides persistent storage for transactions using IndexedDB
193+
// Apply mutations within the transaction
194+
tx.mutate(() => {
195+
// All collection operations (insert/update/delete) within this callback
196+
// will be part of this transaction
197+
})
198+
```
174199

175200
Transactions progress through several states:
176201

@@ -204,35 +229,56 @@ const todosConfig = {
204229
primaryKey: ['id'],
205230
}
206231
),
207-
// Persist mutations to ElectricSQL
208-
mutationFn: async (mutations, transaction, config) => {
209-
const response = await fetch(`http://localhost:3001/api/mutations`, {
210-
method: `POST`,
211-
headers: {
212-
"Content-Type": `application/json`,
213-
},
214-
body: JSON.stringify(transaction.mutations),
232+
};
233+
234+
// In your component
235+
function TodoList() {
236+
const { data, insert, update, delete: deleteFn } = useCollection(todosConfig)
237+
238+
// Create a mutation for handling all todo operations
239+
const todoMutation = useOptimisticMutation({
240+
mutationFn: async ({ transaction }) => {
241+
// Filter out collection from mutations before sending to server
242+
const payload = transaction.mutations.map(m => {
243+
const { collection, ...payload } = m
244+
return payload
245+
})
246+
247+
const response = await fetch(`http://localhost:3001/api/mutations`, {
248+
method: `POST`,
249+
headers: {
250+
"Content-Type": `application/json`,
251+
},
252+
body: JSON.stringify(payload),
253+
})
254+
if (!response.ok) {
255+
// Throwing an error will rollback the optimistic state.
256+
throw new Error(`HTTP error! Status: ${response.status}`)
257+
}
258+
259+
const result = await response.json()
260+
261+
try {
262+
// Use the awaitTxid function from the ElectricSync configuration
263+
// This waits for the specific transaction to be synced to the server
264+
await transaction.mutations[0].collection.config.sync.awaitTxid(result.txid)
265+
} catch (error) {
266+
console.error('Error waiting for transaction to sync:', error);
267+
// Throwing an error will rollback the optimistic state.
268+
throw error;
269+
}
270+
},
271+
})
272+
273+
// Use the mutation for any todo operations
274+
const addTodo = () => {
275+
todoMutation.mutate(() => {
276+
insert({ title: 'New todo', completed: false })
215277
})
216-
if (!response.ok) {
217-
// Throwing an error will rollback the optimistic state.
218-
throw new Error(`HTTP error! Status: ${response.status}`)
219-
}
278+
}
220279

221-
const result = await response.json()
222-
223-
try {
224-
// Use the awaitTxid function from the ElectricSync configuration
225-
// This waits for the specific transaction to be synced to the server
226-
// The second parameter is an optional timeout in milliseconds
227-
await config.sync.awaitTxid(persistResult.txid, 10000)
228-
return true;
229-
} catch (error) {
230-
console.error('Error waiting for transaction to sync:', error);
231-
// Throwing an error will rollback the optimistic state.
232-
throw error;
233-
}
234-
},
235-
};
280+
// ... rest of your component
281+
}
236282

237283
// In a route loader
238284
export async function loader() {

examples/react/todo/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@
4444
"db:generate": "drizzle-kit generate",
4545
"db:push": "tsx scripts/migrate.ts",
4646
"db:studio": "drizzle-kit studio",
47-
"dev": "docker-compose up -d && concurrently \"pnpm api:dev\" \"vite\"",
48-
"lint": "eslint .",
47+
"dev": "docker compose up -d && concurrently \"pnpm api:dev\" \"vite\"",
48+
"lint": "eslint . --fix",
4949
"preview": "vite preview"
5050
},
5151
"type": "module"

0 commit comments

Comments
 (0)