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])
}
}Next Steps
- Relationships - Work with related data
- Offline Handling - Handle network states
- Store API - Complete API reference
Last updated on