# Mutations Inserting, updating, and deleting through a LiveTable, including optimistic updates. Mutators are sync-style and optimistic. The browser updates immediately, the server reconciles asynchronously, and on failure the optimistic UI update is reverted. ```ts comments.insert({ text: "hi", author: "alice" }, () => form.text = ""); comments.update({ ...comment, text: "edited" }, () => setEditing(false)); comments.delete(comment); ``` `insert`, `update`, and `delete` accept a `resetUI` callback as a second argument. It fires as soon as the optimistic change is applied locally, before the server has acknowledged. Use it to clear the state that drove the mutation: form inputs, the row that was being edited, any draft data tied to that action. It is for resetting UI state only, not for navigation or other side effects. Do not navigate or redirect from `resetUI`. The callback runs before the write reaches the server. Navigating there tears the page down and aborts the write, so the row never reaches the database. Put navigation on the line after the mutation instead. The mutators are sync-style, so the compiler awaits the write before the next line runs: ```ts posts.insert({ title, url }); redirect("/"); ``` For a create-then-navigate flow with no on-screen list to update, use a plain `@rpc` that returns the new row rather than `LiveTable.insert`. The optimistic insert is built for adding a row to a list the user is currently looking at; a submit page that creates one record and leaves has no such list, and the `@rpc` avoids the optimistic machinery entirely. That flow can redirect from inside the `@rpc` itself, which removes the timing question altogether. `redirect()` works on the server, and from an `@rpc` the browser navigates only once the call settles, so the write is committed before the page is torn down: ```ts /** @rpc */ export function createPost(title: string, url: string) { let id = sql(`insert into posts (title, url) values (${title}, ${url}) returning id`) .first()!.id; redirect(`/posts/${id}`); } ``` `insertAsync`, `updateAsync`, and `deleteAsync` are explicit-promise variants for genuine promise control (for example, `Promise.all` over several writes). They are not a fix for a mutation that seems not to persist. That is almost always navigation firing too early, addressed by the pattern above. Do not reach for `async`/`await` here: the sync-style mutators already await the write for you. ### Including Every Displayed Field The optimistic row renders before the server reconciles. It holds only the object you pass to `insert(...)`, nothing else. If the template displays `comment.author`, an optimistic `insert({ text })` renders `undefined` for `author` until the server's reply arrives. Pass every field the template reads at insert time, even if the server canonicalizes them. ```ts // browser shows undefined author until reconcile comments.insert({ text: form.text }); // browser shows the author immediately comments.insert({ text: form.text, author: session.get("userName")! }); ``` This includes server-generated columns the template reads: timestamps like `createdAt` (`default now()`), sequence numbers, and anything a database trigger fills in. Those values exist only after the row round-trips, so the optimistic row has them as `undefined` until reconcile. A displayed `createdAt` is the common trap: `new Date(undefined)` renders `Invalid Date` for the first frames, then corrects itself once the broadcast lands, which reads as a flicker with no obvious cause. Pass a client-side placeholder that the server's real value reconciles a moment later. ```ts // createdAt is undefined until reconcile: new Date(undefined) is Invalid Date comments.insert({ text: form.text, author: session.get("userName")! }); // pass a client-side placeholder; the server's now() reconciles it comments.insert({ text: form.text, author: session.get("userName")!, createdAt: new Date(), }); ```