Skip to Content

Mutations

Create, update, and delete data with optimistic updates.

API Reference: store.save · store.create · store.remove

Update (Save)

Basic Update

import { observer, useStore } from '@gluonic/client' const PostEditor = observer(({ postId }) => { const store = useStore() const post = useModel<Post>('post', postId) const handleSave = async () => { await store.save('post', postId, { title: 'New Title', content: 'New Content' }) // UI updates immediately ✓ // Syncs in background ✓ } return <button onClick={handleSave}>Save</button> })

Partial Updates

// Update just one field await store.save('post', postId, { title: 'New Title' }) // Update multiple fields await store.save('post', postId, { title: 'New Title', content: 'New Content', published: true })

Field-Level Updates

const TitleEditor = observer(({ post }) => { const store = useStore() return ( <input value={post.title} onChange={e => { // Updates on every keystroke (debounced internally) store.save('post', post.id, { title: e.target.value }) }} /> ) })

Create

Basic Creation

const CreatePost = () => { const store = useStore() const router = useRouter() const handleCreate = async () => { const id = generateId() // cuid, uuid, etc. await store.create('post', id, { title: 'New Post', content: '', authorId: currentUserId, published: false }) // Navigate to new post router.push(`/posts/${id}`) } return <button onClick={handleCreate}>New Post</button> }

With Default Values

// Model with defaults @ClientModel('post') export class Post extends Model { @Property({ defaultOn: ['create'], defaultProvider: () => Date.now() }) createdAt: number = 0 } // Create (default applied automatically) await store.create('post', id, { title: 'New Post' // createdAt auto-set to Date.now() ✓ })

Delete

Basic Deletion

const DeleteButton = observer(({ postId }) => { const store = useStore() const handleDelete = async () => { if (confirm('Delete this post?')) { await store.remove('post', postId) router.back() } } return <button onClick={handleDelete}>Delete</button> })

Cascade Deletes

// Delete post and its comments const handleDeletePost = async (postId) => { // Get comments first const post = bridge.getModel<Post>('post', postId) const comments = post.comments.toArray() // Delete comments for (const comment of comments) { await store.remove('comment', comment.id) } // Delete post await store.remove('post', postId) }

Batch Mutations

Multiple mutations in same tick are batched:

// These are batched into single server request await store.save('post', '1', { title: 'A' }) await store.save('post', '2', { title: 'B' }) await store.save('post', '3', { title: 'C' }) // Sent as: POST /sync/v1/tx { ops: [ { t: 'post', id: '1', op: 'u', patch: { title: 'A' } }, { t: 'post', id: '2', op: 'u', patch: { title: 'B' } }, { t: 'post', id: '3', op: 'u', patch: { title: 'C' } } ] }

Error Handling

Mutation Errors

try { await store.save('post', postId, { title: 'New' }) } catch (error) { // Errors are thrown if: // - Network completely down (offline) // - Server returns error (conflict, forbidden, etc.) if (error.message.includes('conflict')) { alert('This post was modified by someone else') } else if (error.message.includes('forbidden')) { alert('You don\'t have permission to edit this') } else { alert('Failed to save') } }

Automatic Rollback

// User edits post await store.save('post', '123', { title: 'New' }) // UI shows "New" immediately (optimistic) // Server rejects (version conflict) // Pool automatically rolls back to previous value // UI shows old title again // No manual rollback needed ✓

Optimistic UI

Show Pending State

const PostTitle = observer(({ post }) => { const isPending = post.isFieldOptimistic('title') return ( <div className={isPending ? 'opacity-70' : ''}> <h1>{post.title}</h1> {isPending && <Spinner size="sm" />} </div> ) })

Disable During Sync

const SaveButton = observer(({ post }) => { const { isSyncing } = useConnectionState() return ( <button disabled={isSyncing || post.optimisticFields.length > 0} onClick={handleSave} > Save </button> ) })

Advanced Patterns

Debounced Mutations

import { debounce } from 'lodash' const AutoSaveEditor = observer(({ post }) => { const store = useStore() const debouncedSave = useMemo( () => debounce((title) => { store.save('post', post.id, { title }) }, 500), [post.id] ) return ( <input value={post.title} onChange={e => { debouncedSave(e.target.value) }} /> ) })

Conditional Mutations

const handlePublish = async () => { // Validate before mutating if (post.title.length === 0) { alert('Title required') return } if (post.content.length < 100) { alert('Content too short') return } // All good - publish await store.save('post', post.id, { published: true, publishedAt: Date.now() }) }

Undo/Redo

const [history, setHistory] = useState<any[]>([]) const handleEdit = async (patch) => { // Save current state setHistory([...history, post.toJSON()]) // Apply edit await store.save('post', post.id, patch) } const handleUndo = async () => { const prev = history.pop() if (prev) { await store.save('post', post.id, prev) setHistory([...history]) } }

API Reference: useStore · observer

Next Steps

Last updated on