@@ -117,12 +117,13 @@ Mutations are based on a `Transaction` primitive.
117117
118118For simple state changes, directly mutating the collection and persisting with the operator handlers is enough.
119119
120- But for more complex use cases, you can directly create custom mutators with ` useOptimisticMutation ` . This lets you do things such as do transactions with multiple mutations across multiple collections, do chained transactions w/ intermediate rollbacks, etc.
120+ But for more complex use cases, you can directly create custom actions with ` createOptimisticAction ` or custom transactions with ` createTransaction ` . This lets you do things such as do transactions with multiple mutations across multiple collections, do chained transactions w/ intermediate rollbacks, etc.
121121
122122For example, in the following code, the mutationFn first sends the write to the server using ` await api.todos.update(updatedTodo) ` and then calls ` await collection.refetch() ` to trigger a re-fetch of the collection contents using TanStack Query. When this second await resolves, the collection is up-to-date with the latest changes and the optimistic state is safely discarded.
123123
124124``` ts
125- const updateTodo = useOptimisticMutation ({
125+ const updateTodo = createOptimisticAction <{id: string }>({
126+ onMutate ,
126127 mutationFn : async ({ transaction }) => {
127128 const { collection, modified : updatedTodo } = transaction .mutations [0 ]
128129
@@ -384,55 +385,83 @@ const mutationFn: MutationFn = async ({ transaction }) => {
384385}
385386` ` `
386387
387- #### ` useOptimisticMutation `
388+ #### ` createOptimisticAction `
388389
389- Use the ` useOptimisticMutation ` hook with your ` mutationFn ` to create a mutator that you can use to mutate data in your components :
390+ Use ` createOptimisticAction ` with your ` mutationFn ` and ` onMutate ` functions to create an action that you can use to mutate data in your components in fully custom ways :
390391
391392` ` ` tsx
392- import { useOptimisticMutation } from '@tanstack/react-db'
393+ import { createOptimisticAction } from '@tanstack/react-db'
394+
395+ // Create the ` addTodo ` action, passing in your ` mutationFn ` and ` onMutate ` .
396+ const addTodo = createOptimisticAction<string>({
397+ onMutate: (text) => {
398+ // Instantly applies the local optimistic state.
399+ todoCollection.insert({
400+ id: uuid(),
401+ text,
402+ completed: false
403+ })
404+ },
405+ mutationFn: async (text) => {
406+ // Persist the todo to your backend
407+ const response = await fetch('/api/todos', {
408+ method: 'POST',
409+ body: JSON.stringify({ text, completed: false }),
410+ })
411+ return response.json()
412+ }
413+ })
393414
394415const Todo = () => {
395- // Create the ` addTodo ` mutator, passing in your ` mutationFn ` .
396- const addTodo = useOptimisticMutation({ mutationFn })
397-
398416 const handleClick = () => {
399- // Triggers the mutationFn
400- addTodo.mutate(() =>
401- // Instantly applies the local optimistic state.
402- todoCollection.insert({
403- id: uuid(),
404- text: '🔥 Make app faster',
405- completed: false
406- })
407- )
417+ // Triggers the onMutate and then the mutationFn
418+ addTodo('🔥 Make app faster')
408419 }
409420
410421 return <Button onClick={ handleClick } />
411422}
412423` ` `
413424
414- Transaction lifecycles can be manually controlled :
425+ ## Manual Transactions
426+
427+ By manually creating transactions , you can fully control their lifecycles and behaviors . ` createOptimisticAction ` is a ~ 25 line
428+ function which implements a common transaction pattern. Feel free to invent your own patterns!
429+
430+
431+ Here ' s one way you could use transactions.
415432
416433` ` ` ts
417- const addTodo = useOptimisticMutation({ mutationFn })
418- const tx = addTodo.createTransaction()
434+ import { createTransaction } from "@tanstack/react-db"
419435
420- tx.mutate(() => {}
436+ const addTodoTx = createTransaction({
437+ autoCommit: false,
438+ mutationFn: async ({ transaction }) => {
439+ // Persist data to backend
440+ await Promise.all(transaction.mutations.map(mutation => {
441+ return await api.saveTodo(mutation.modified)
442+ })
443+ },
444+ })
445+
446+ // Apply first change
447+ addTodoTx.mutate(() => todoCollection.insert({ id: '1', text: 'First todo', completed: false }))
421448
422449// user reviews change
423450
424- // Another mutation
425- tx .mutate(() => {}
451+ // Apply another change
452+ addTodoTx .mutate(() => todoCollection.insert({ id: '2', text: 'Second todo', completed: false }))
426453
427- // Mutation is approved
428- tx .commit()
454+ // User decides to save and we call .commit() and the mutations are persisted to the backend.
455+ addTodoTx .commit()
429456` ` `
430457
458+ ## Transaction lifecycle
459+
431460Transactions progress through the following states :
432461
4334621. ` pending ` : Initial state when a transaction is created and optimistic mutations can be applied
4344632. ` persisting ` : Transaction is being persisted to the backend
435- 3. ` completed ` : Transaction has been successfully persisted
464+ 3. ` completed ` : Transaction has been successfully persisted and any backend changes have been synced back .
4364654. ` failed ` : An error was thrown while persisting or syncing back the Transaction
437466
438467#### Write operations
@@ -569,10 +598,7 @@ This pattern allows you to extend an existing TanStack Query application, or any
569598
570599One of the most powerful ways of using TanStack DB is with a sync engine , for a fully local - first experience with real - time sync . This allows you to incrementally adopt sync into an existing app , whilst still handling writes with your existing API .
571600
572- Here , we illustrate this pattern :
573-
574- - using [ElectricSQL ](https :// electric-sql.com) as the sync engine; and
575- - Instead of sending mutations to unique endpoints , we show an example here of how to POST mutations to a generic ` /ingest/mutations ` endpoint
601+ Here , we illustrate this pattern using [ElectricSQL ](https :// electric-sql.com) as the sync engine.
576602
577603` ` ` tsx
578604import type { Collection } from '@tanstack/db'
@@ -592,71 +618,25 @@ export const todoCollection = createCollection(electricCollectionOptions<Todo>({
592618 },
593619 getKey: (item) => item.id,
594620 schema: todoSchema
595- }))
596-
597- // Define a generic ` mutationFn ` that handles all mutations and
598- // POSTs them to a backend ingestion endpoint.
599- const mutationFn: MutationFn = async ({ transaction }) => {
600- const payload = transaction.mutations.map((mutation: PendingMutation) => {
601- const { collection: _, ...rest } = mutation
602-
603- return rest
604- })
605-
606- const response = await fetch('/ingest/mutations', {
607- method: 'POST',
608- headers: {
609- 'Content-Type': 'application/json',
610- },
611- body: JSON.stringify(payload)
612- })
621+ onInsert: ({ transaction }) => {
622+ const response = await api.todos.create(transaction.mutations[0].modified)
613623
614- if (!response.ok) {
615- // Throwing an error will rollback the optimistic state.
616- throw new Error( ` HTTP Error : $ {response.status }` )
624+ return { txid: response.txid}
617625 }
626+ // You can also implement onUpdate, onDelete as needed.
627+ }))
618628
619- const result = await response.json()
620-
621- // Wait for the transaction to be synced back from the server
622- // before discarding the optimistic state.
623- const collection: Collection = transaction.mutations[0].collection
624-
625- // Here we use an ElectricSQL feature to monitor the incoming sync
626- // stream for the database transaction ID that the writes we made
627- // were applied under. When this syncs through, we can safely
628- // discard the local optimistic state.
629- await collection.awaitTxId(result.txid)
630- }
631-
632- // We can now use the same mutationFn for any local write operations.
633629const AddTodo = () => {
634- const addTodo = useOptimisticMutation({ mutationFn })
635-
636630 return (
637631 <Button
638632 onClick={() =>
639- addTodo.mutate(() =>
640- todoCollection.insert({
641- id: uuid(),
642- text: "🔥 Make app faster",
643- completed: false
644- })
645- )
633+ todoCollection.insert({ text: "🔥 Make app faster" })
646634 }
647635 />
648636 )
649637}
650638` ` `
651639
652- The key requirements for the server in this case are :
653-
654- 1. to be able to parse and ingest the payload format
655- 2. to return the database transaction ID that the changes were applied under ; this then allows the mutationFn to monitor the replication stream for that ` txid ` , at which point the local optimistic state is discarded
656-
657- > [! TIP ]
658- > One reference implementation of a backend designed to ingest changes from TanStack DB is the [` Phoenix.Sync.Writer ` ](https :// hexdocs.pm/phoenix_sync/Phoenix.Sync.Writer.html) module from the [Phoenix.Sync](https://hexdocs.pm/phoenix_sync) library for the [Phoenix web framework](https://www.phoenixframework.org).
659-
660640## More info
661641
662642If you have questions / need help using TanStack DB , let us know on the Discord or start a GitHub discussion :
0 commit comments