Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
63980d5
New transaction types & remove mutationFn from collection.ts
KyleAMathews May 5, 2025
62622fb
Add transaction registry
KyleAMathews May 5, 2025
27532fb
tests passing
KyleAMathews May 6, 2025
0733ee0
Merge remote-tracking branch 'origin/main' into kylemathews/1st-class…
KyleAMathews May 6, 2025
1387613
fix typs
KyleAMathews May 6, 2025
f7e2b31
fix react-optimistic
KyleAMathews May 6, 2025
3b5aedf
fix example app
KyleAMathews May 6, 2025
401000c
Update example app & fix a few things
KyleAMathews May 6, 2025
fb07d82
Fix type errors
KyleAMathews May 7, 2025
b3e9c82
new mutations to the same key should overwrite older ones in the same…
KyleAMathews May 7, 2025
78b2e99
add test for manually managing transaction lifecycle
KyleAMathews May 7, 2025
5e8571c
add test for mutating multiple collections within a transaction
KyleAMathews May 7, 2025
81daec6
Add support for rolling back transactions
KyleAMathews May 7, 2025
bdfbbdf
test that error is thrown when trying to use a collection mutator out…
KyleAMathews May 7, 2025
7ddf4a4
Rollback on errors from mutationFn
KyleAMathews May 7, 2025
75e200d
add useOptimisticMutation
KyleAMathews May 7, 2025
bab9548
update READMEs
KyleAMathews May 7, 2025
30f802e
prettier
KyleAMathews May 7, 2025
de69b44
move useOptimisticMutation to its own file
KyleAMathews May 7, 2025
5ffc936
Fix types
KyleAMathews May 7, 2025
ec118d0
fix import
KyleAMathews May 7, 2025
8fdc407
testing
KyleAMathews May 7, 2025
a79e2e5
fix build
KyleAMathews May 7, 2025
373e8b1
get collection to re-render after rollback
KyleAMathews May 7, 2025
a0a0e52
Fix test
KyleAMathews May 7, 2025
a0374e3
debugging
KyleAMathews May 7, 2025
c27ffbc
debuggin
KyleAMathews May 7, 2025
f7109d7
more debugging
KyleAMathews May 8, 2025
12ff4be
Remove debugging
KyleAMathews May 8, 2025
8e8b6b1
Fixes & tests
KyleAMathews May 8, 2025
353e909
Fix useLiveQuery in strict mode, and fix query types in built package
samwillis May 8, 2025
50d38b6
Finish updating todo example
KyleAMathews May 8, 2025
dd1852d
Make state public
KyleAMathews May 8, 2025
fcc9395
one more fix
KyleAMathews May 8, 2025
8926b2c
Add changeset
KyleAMathews May 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/sweet-kings-wear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@tanstack/react-optimistic": patch
"@tanstack/optimistic": patch
---

Make transactions first class & move ownership of mutationFn from collections to transactions
108 changes: 77 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,25 @@ The library uses proxies to create immutable snapshots and track changes:
The primary hook for interacting with collections in React components.

```typescript
// Create a collection
const { data, insert, update, delete: deleteFn } = useCollection({
id: 'todos',
sync: { /* sync configuration */ },
mutationFn: { /* mutation functions */ },
schema: /* optional schema */
});

// Create a mutation
const mutation = useOptimisticMutation({
mutationFn: async ({ mutations }) => {
// Implement your mutation logic here
// This function is called when mutations are committed
}
});

// Use the mutation with collection operations
mutation.mutate(() => {
insert({ text: 'New todo' });
});
```

Returns:
Expand Down Expand Up @@ -167,10 +180,22 @@ const todoCollection = useCollection({

## Transaction Management

The library includes a robust transaction management system:
The library includes a simple yet powerful transaction management system. Transactions are created using the `createTransaction` function:

```typescript
const tx = createTransaction({
mutationFn: async ({ transaction }) => {
// Implement your mutation logic here
// This function is called when the transaction is committed
},
})

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

Transactions progress through several states:

Expand Down Expand Up @@ -204,35 +229,56 @@ const todosConfig = {
primaryKey: ['id'],
}
),
// Persist mutations to ElectricSQL
mutationFn: async (mutations, transaction, config) => {
const response = await fetch(`http://localhost:3001/api/mutations`, {
method: `POST`,
headers: {
"Content-Type": `application/json`,
},
body: JSON.stringify(transaction.mutations),
};

// In your 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 }) => {
// Filter out collection from mutations before sending to server
const payload = transaction.mutations.map(m => {
const { collection, ...payload } = m
return payload
})

const response = await fetch(`http://localhost:3001/api/mutations`, {
method: `POST`,
headers: {
"Content-Type": `application/json`,
},
body: JSON.stringify(payload),
})
if (!response.ok) {
// Throwing an error will rollback the optimistic state.
throw new Error(`HTTP error! Status: ${response.status}`)
}

const result = await response.json()

try {
// Use the awaitTxid function from the ElectricSync configuration
// This waits for the specific transaction to be synced to the server
await transaction.mutations[0].collection.config.sync.awaitTxid(result.txid)
} catch (error) {
console.error('Error waiting for transaction to sync:', error);
// Throwing an error will rollback the optimistic state.
throw error;
}
},
})

// Use the mutation for any todo operations
const addTodo = () => {
todoMutation.mutate(() => {
insert({ title: 'New todo', completed: false })
})
if (!response.ok) {
// Throwing an error will rollback the optimistic state.
throw new Error(`HTTP error! Status: ${response.status}`)
}
}

const result = await response.json()

try {
// Use the awaitTxid function from the ElectricSync configuration
// This waits for the specific transaction to be synced to the server
// The second parameter is an optional timeout in milliseconds
await config.sync.awaitTxid(persistResult.txid, 10000)
return true;
} catch (error) {
console.error('Error waiting for transaction to sync:', error);
// Throwing an error will rollback the optimistic state.
throw error;
}
},
};
// ... rest of your component
}

// In a route loader
export async function loader() {
Expand Down
4 changes: 2 additions & 2 deletions examples/react/todo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@
"db:generate": "drizzle-kit generate",
"db:push": "tsx scripts/migrate.ts",
"db:studio": "drizzle-kit studio",
"dev": "docker-compose up -d && concurrently \"pnpm api:dev\" \"vite\"",
"lint": "eslint .",
"dev": "docker compose up -d && concurrently \"pnpm api:dev\" \"vite\"",
"lint": "eslint . --fix",
"preview": "vite preview"
},
"type": "module"
Expand Down
Loading