Optimistic updates for debounced mutations
I would like to have an optimistic update where the mutation is debounced (not single-flighted, but debounced)
My usecase: When user types text in the field, the text should get saved in the database, but only after 1 sec after the user stopped typing. Meanwhile, another user looking at the same page should see the edited text as it gets updated in the database. Kind of like Google Doc.
Can I have no local
useState
and just have the value as useQuery
result, and apply mutation withOptimisticUpdate
? But how to introduce debouncing?
Alternatively, I could keep local useState
, and have my custom debounce()
wrapper which sets timeout and so on. And wrap mutation with that, and call debouncedMutation
onChange. But then what about the other user? Should I Watch
the query and update local state there?7 Replies
Optimistic updates should be one-to-one with real mutations, so I would keep the
useState
and use a debounced mutation.
But then what about the other user? Should I Watch the query and update local state there?You're going to need to "rebase" another user's changes when they have a not-yet-committed change. Optimistic updates in Convex automatically reapply when new query results arrive, but - your useState changes won't be reapplied like this, and - the optimistic updates would be a problem anyway, they're too coarse If you want really collaborative editing, you probably want ProseMirror collab or similar it does content-aware diffs, taking care of the rebase step aka operational transforms But simpler would be "informal locking" or whatever folks call it, where you show that someone is currently typing in a text box
Suppose I don't do
withOptimisticUpdate
and let's say I resolve conflicts like "local version trumps remote version if user is typing, remote trumps local otherwise" (poor man's OT/CRDT lol). Then how would the non-typing user get updates? How do I bound their local state with query result?I would make the useState part of your debounced mutation, it should reset as soon as there is no more pending mutation
const {state, setState} useQueryOrLocalStateIfPending()
and the implementation of this custom hook uses useQuery("textBoxState")
Let me know if more details would be helpful. Without testing it, something like this
Thanks a lot! I'll experiment and report back. Haven't written custom hooks before
You can do this directly in a component too, custom hooks are just for abstracting code from components
@ballingt This works almost verbatim!
I made
debouncedMut
also setLocalState
before clearTimeout
.
And also swapped mut(val)
with setTimer(null)
so that the typing user doesn't have a microglitch while timer is already null but mutation+query has not applied+refreshed yet.this makes me think of https://youtu.be/cCOL7MC4Pl0?t=107, I don't think the second swap is necessary but I agree with you and Jake in the video, I would still swap it
in this case I think setState may synchronously cause a render, but it will not update the screen until the mut(val) been invoked too
Oh and I guess this code as I wrote it requires that you implement optimistic updates too!
If you don't, you'll want to wait until the promise resolves to stop using local state