jejunum
jejunumβ€’12mo ago

useQuery Rerendering whenever new data is patched

I have a rich text editor that is pushing data to my convexDB:
const EditorComponent = () => {
const updateContentDebounced = useCallback(_.debounce((content) => {
updateContent({storageId: _storageId, content: JSON.stringify(content)})
}, 5000), [_storageId, updateContent]);

const editor: BlockNoteEditor = useBlockNote({
initialContent: initialContent ? JSON.parse(initialContent) : undefined,
onEditorContentChange: (editor) => {
updateContentDebounced(editor.topLevelBlocks);
},
});

return <BlockNoteView editor={editor} theme="light"/>;
};
const EditorComponent = () => {
const updateContentDebounced = useCallback(_.debounce((content) => {
updateContent({storageId: _storageId, content: JSON.stringify(content)})
}, 5000), [_storageId, updateContent]);

const editor: BlockNoteEditor = useBlockNote({
initialContent: initialContent ? JSON.parse(initialContent) : undefined,
onEditorContentChange: (editor) => {
updateContentDebounced(editor.topLevelBlocks);
},
});

return <BlockNoteView editor={editor} theme="light"/>;
};
The below is my updateContent:
export const updateContent = mutation({
args: {
storageId: v.optional(v.union(v.null(), v.id("node"))),
content: v.string(),
},

handler: async (ctx, args) => {
if (args.storageId == null) {
return null;
}
const node = await ctx.db.patch(args.storageId,
{
content: args.content,
});
},
})
export const updateContent = mutation({
args: {
storageId: v.optional(v.union(v.null(), v.id("node"))),
content: v.string(),
},

handler: async (ctx, args) => {
if (args.storageId == null) {
return null;
}
const node = await ctx.db.patch(args.storageId,
{
content: args.content,
});
},
})
The issue lies on each updateContent call, a mutation to the database is made and my front end component then seems to rerender, push a new line & unfocuses. Is this a behaviour of useQuery since it rerenders whenver a query result changes? Keen to hear if there are any ways to navigate this! Thanks πŸ™‚
No description
10 Replies
lee
leeβ€’12mo ago
Hi! You're right that useQuery will rerender whenever data changes. You can adjust this behavior with a wrapper like useStableQuery or useBufferedState, described here https://stack.convex.dev/help-my-app-is-overreacting
Help, my app is overreacting!
Reactive backends like Convex make building live-updating apps a cinch, but default behavior might be too reactive for some use cases. Not to worry! L...
lee
leeβ€’12mo ago
GitHub
convex-buffered-state/pages/index.tsx at main Β· jamwt/convex-buffer...
Contribute to jamwt/convex-buffered-state development by creating an account on GitHub.
Managing Reactivity with useBufferedState
Reactivity has taken a dominant position today within web app development. Our components and app state are all reactive, and the world has adapted–mo...
jejunum
jejunumOPβ€’12mo ago
Hi @lee you are pretty much spot on! Thanks so much for the above resources as well! Out of interest, why do you say that useBufferedState is more appropriate for my use case instead of useStableQuery? Not 100% sure what the differences are in their behaviour and why one may be more appropriate than the other 🫑
lee
leeβ€’12mo ago
useStableQuery is for when a query's arguments are changing, and you want to hold onto the old query result while the new one is calculating. It avoids the useQuery becoming undefined. useBufferedState is for when a query is rerunning because of changing underlying data. It allows you to hold onto the old query result for as long as the client wants, avoiding rerendering new data until an explicit sync request. In addition to the above, there may be different solutions where the rerendering does not unfocus the view. Some people have built collaborative text editing on Convex, where data is synced both ways without disrupting either editor. (I believe @RJ has an example. And cc @ian who has looked into this)
jejunum
jejunumOPβ€’12mo ago
Thanks so much Lee, ur an absolute legend πŸ‘ @RJ would you mind sharing the example that you have please?
RJ
RJβ€’12mo ago
Gladly @jejunum, you can find it here: https://github.com/rjdellecese/scroll. I'm not sure if the live site is working anymore as it's using a very old version of Convex, but the Convex code should still be pretty easy to follow. Check out the README for details and feel free to ask me any questions, if you have any!
winz
winzβ€’10mo ago
Hello @RJ , Thanks so much for sharing the code, it is a very cool app! I am just trying to understand how you did autosave without blurring the text editor. I see the update function and am guessing this is called every time someone stops typing? But I can't seem to find when this function is called. Are you able to point me the right direction? Thank you!
RJ
RJβ€’10mo ago
Hey @winz, sure thing! The Tiptap editor is initialized with a parameter specifying that we should dispatch a message onTransactionβ€”you can see that here (https://github.com/rjdellecese/scroll/blob/15f923738ecf0597a8bcd877d6af790b48bb2e5f/src/elm-ts/note.tsx#L583-L590). So every time a transaction is applied to the editor (meaning every time the editor state has changed), this branch of the update function is executed: https://github.com/rjdellecese/scroll/blob/15f923738ecf0597a8bcd877d6af790b48bb2e5f/src/elm-ts/note.tsx#L338-L358. This does some debouncing (by checking areStepsInFlight) to check that we aren't currently running a mutation to save the latest changes (which are recorded as steps). If we aren't, it will check to see whether there are "sendable steps" (meaning, whether there are local changes that have not been persisted to the server yet), and send them if there are. If you aren't familiar at all with how ProseMirror's collab module works, I strongly recommend reading https://prosemirror.net/docs/guide/#collab first. If you don't understand the algorithm that ProseMirror uses for collaborative editing (at least at a high level), you'll probably have trouble following the code in Scroll! Once Convex releases its much-anticipated "components" feature, hopefully I'll be able to bundle this up more nicely for folks so that they don't need to understand as much to get rolling πŸ™‚
David Alonso
David Alonsoβ€’4mo ago
Very glad I stumbled across this thread. I have a few questions: 1. If we're using useCachedAuthQuery (custom auth query built on top of useCachedQuery) in places where we'd like behavior of useStableQuery and useBufferedState, should we still check the links shared before or are there any new helpers? e.g. something like makeUseStableQuery or something like that. This is what I have right now:
export function useCachedAuthStableQueryWithStatus<Query extends AuthFunction<"query">>(
query: Query,
...args: AuthQueryArgsArray<Query>
): WithQueryStatus<FunctionReturnType<Query>> {
const result = useCachedAuthQueryWithStatus(query, ...args);
const stored = useRef(result);

if (result.status !== "pending") {
stored.current = result;
}

return stored.current;
}
export function useCachedAuthStableQueryWithStatus<Query extends AuthFunction<"query">>(
query: Query,
...args: AuthQueryArgsArray<Query>
): WithQueryStatus<FunctionReturnType<Query>> {
const result = useCachedAuthQueryWithStatus(query, ...args);
const stored = useRef(result);

if (result.status !== "pending") {
stored.current = result;
}

return stored.current;
}
2. We're using optimistic updates in a bunch of places and noticed that components rerender when the server value is received. Will this only happen if the query returns a value that is not exactly the same as the optimistic update query (incl system fields)? Any thoughts?
ian
ianβ€’3mo ago
1. Yeah rolling your own hooks make sense for now, when combining hooks. I shy away from useStableQuery since arg changes might be meaningful and not safe as drop-in replacements. You might want to return stale data alongside the new data, as an alternative API, so you can show stale data as non-interactive / stale. 2. It will likely re-render even if the update matches the optimistic update. I don't believe there is a deep object comparison before triggering a re-render. However, wherever you de-structure the values, regular props caching will apply. So if you pass props based on the underlying values, none of those will re-render if the props don't change.

Did you find this page helpful?