Feature Request: API for checking if query/mutation/action is loading

In general, there are workarounds like erquhart pointed out in this thread: https://discord.com/channels/1019350475847499849/1220751135514824764 These workarounds are totally working, but my big issue with these is that I always have to differentiate between null and undefined and need to remember which one of these two represents the loading and which is the "no answer" case. This is not only an overhead for the client but also an overhead for writing the backend code since you can't just return one of these but instead have to think through which one of these represents the loading state so you have to return the other one. I also just could return "no data" but then it gets messier on the client and is not my way of doing things. Just quality of life feedback.
100 Replies
FleetAdmiralJakob πŸ—• πŸ—— πŸ—™
I also read this "zen of convex" but I think the key takeaway from them is that we should not care much about state management etc. so I think this feature would me help think less about this stuff and just call the api
No description
FleetAdmiralJakob πŸ—• πŸ—— πŸ—™
You are especially calling out that my return values should not affect my state so if I have to return "no answer" or something like that to trigger (not trigger) a loading state this is exactly the opposite of this zen
No description
FleetAdmiralJakob πŸ—• πŸ—— πŸ—™
tanstack query also has awesome docs on how they do the status management: https://tanstack.com/query/latest/docs/framework/react/guides/queries
Abhishek
Abhishekβ€’9mo ago
Plus one on this
ballingt
ballingtβ€’9mo ago
We hear you! We've talked about this for years and it never makes the priority list because you can write wrappers that handle it. But it's helpful to hear this is a stumbling block. I don't want to add every option in tanstack query (and they've removed some over time) so it's especially helpful to hear specific requests like your above, that an isLoading would be helpful.
Abhishek
Abhishekβ€’9mo ago
It would be very helpful if the isLoading functionality could be implemented.
ballingt
ballingtβ€’9mo ago
@FleetAdmiralJakob πŸ—• πŸ—— πŸ—™ re
an overhead for writing the backend code since you can't just return one of these but instead have to think through which one of these represents the loading state so you have to return the other one.
You can return whatever you want on the server, undefined still always means "loading" on the client. If you return undefined (or not return anything, same thing) from a Convex query function it'll be turned into null so you can tell the difference. To either of you or anyone else, is there anything else besides const { isLoading, data } = useQueryNew(...) instead of
const result = useQuery(...);
const isLoading = result === undefined;
const result = useQuery(...);
const isLoading = result === undefined;
that's painful to be missing?
Abhishek
Abhishekβ€’9mo ago
How about we can add an error field too if possible
FleetAdmiralJakob πŸ—• πŸ—— πŸ—™
Oh, I did not know that But I don't think this should be that way, this can be very confusing even if this is good documented. If I return something I want that exactly this comes out of the client. An isLoading/status and error api would help remove this conversion from undefined to null.
Abhishek
Abhishekβ€’9mo ago
With the introduction of new fields such as isLoading, React Suspense can work with useQuery without requiring a manual hook, which will be very useful.
ballingt
ballingtβ€’9mo ago
That makes sense. For the foreseeable future we can't transfer a undefined from a Convex function because many languages don't have two different values for undefined and null β€”Β and we want Convex values to be generally useful when called from all clients. Instead of erroring on undefined (inconvenient since that's the default return type of functions in JavaScript) we convert it to null. React Suspense is a different feature, it sounds like you'd like that too? or are you saying you'd implement the suspense part on top of an isLoading?
Abhishek
Abhishekβ€’9mo ago
If convex can provide it out of the box that would save a lot of time
FleetAdmiralJakob πŸ—• πŸ—— πŸ—™
How do you guys do it in other languages with the loading states if there are no null and undefined separations?
ballingt
ballingtβ€’9mo ago
That's really just for React, in e.g. the plain JavaScript client a function runs every time there's an update In other reactive UI frameworks for TypeScript I think we'd go with an isLoading since TypeScript makes this kind of thing slick in other languages I think we'd do an enum or an option type
FleetAdmiralJakob πŸ—• πŸ—— πŸ—™
Looking at the old threads covering that topic it seems like a high demand feature
ballingt
ballingtβ€’9mo ago
Yeah, several community implementions have made it feel less critical but it's good to hear it would be helpful to have something more official
FleetAdmiralJakob πŸ—• πŸ—— πŸ—™
But for now this is solved for me, I thought I have to remember the difference between null and undefined but now that I know that undefined always means loading I think everything is easier.
Dima Utkin
Dima Utkinβ€’9mo ago
@FleetAdmiralJakob πŸ—• πŸ—— πŸ—™ i'm pretty sure that having a (semi-)polished experience for a production app would require you to have tanstack-query as a wrapper for ConvexHttpClient or ConvexReactClient, imho, for majority of your queries
Abhishek
Abhishekβ€’9mo ago
Not necessary, sometime Tanstack query feels overkill and over the year working with nextjs I don't think it is that important, but still this is my opinion
Dima Utkin
Dima Utkinβ€’9mo ago
true, the nature of the website\app is very important here. but i can hardly imagine creating a nice UX just for basic "navigate back to the previous page" experience, without utilizing some sort of a client-side cache(if we're talking SPA) default useQuery from convex/react won't be any of help in this case πŸ€·β€β™‚οΈ
Dima Utkin
Dima Utkinβ€’9mo ago
and there's a good post on Convex Stack about another classic issue of pure reactive systems https://stack.convex.dev/help-my-app-is-overreacting so, wrappers are very much needed sometimes
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...
Khalil
Khalilβ€’9mo ago
+1 on this feature request from me! this was the ONLY downgrade I faced when migrating from tRPC + react query to convex. I do miss the isLoading from queries/mutations
FleetAdmiralJakob πŸ—• πŸ—— πŸ—™
You can't build something without something like Tanstack Query. But that's not a reason to bring it into a convex app. Convex should learn things from Tanstack Query and build there own deeply integrated apis. So convex should be a replacement of tanstack query And this new api(s) (error and loading) would help convex to replace tanstack query
ian
ianβ€’9mo ago
anyone written a helper hook yet that can be used like const { data, isLoading}= useLoading(useQuery(...)); ? Would that be enough for now @Khalil ? or a single hook that wraps useQuery and mimics the useQuery types
Khalil
Khalilβ€’9mo ago
I have not written a helper hook; I can try it. I think the challenging part would be to make it type-safe with args and return type. In this video from Code with Antonio, you can see he also wanted this feature and built his own hook, but lost type-safety https://youtu.be/ADJKbuayubE?si=ehMdITgAMa1q1Lky&t=9193
aheimlich
aheimlichβ€’9mo ago
I wonder if you might be able to use useAsync() from @react-hookz/web to help here (at least with mutations and actions)? https://react-hookz.github.io/web/?path=/docs/side-effect-useasync--example
ian
ianβ€’9mo ago
This works for me:
export function useLoading<T>(value: T | undefined):
| {
value: T;
isLoading: false;
}
| { value: undefined; isLoading: true } {
if (value === undefined) {
return { value: undefined, isLoading: true };
} else {
return { value, isLoading: false };
}
}
export function useLoading<T>(value: T | undefined):
| {
value: T;
isLoading: false;
}
| { value: undefined; isLoading: true } {
if (value === undefined) {
return { value: undefined, isLoading: true };
} else {
return { value, isLoading: false };
}
}
@Khalil @aheimlich lmk how it works for you You can rename value to data or w/e. I'll add this & other utilities to convex-helpers that include memoization too
FleetAdmiralJakob πŸ—• πŸ—— πŸ—™
Maybe this needs more work, but can we extend this to add an error value if the status is error?
Abhishek
Abhishekβ€’9mo ago
single hook that wraps useQuery and mimics the useQuery types , would be the one that everyone wants
FleetAdmiralJakob πŸ—• πŸ—— πŸ—™
This is just for queries, not for mutations, right?
erquhart
erquhartβ€’9mo ago
Yeah you'd have to use a different pattern for mutations. It'd be ideal for the helpers to wrap the entire useQuery/useMutation hook to better approximate the tanstack/swr/redux-hooks style API.
FleetAdmiralJakob πŸ—• πŸ—— πŸ—™
I agree completely, this solution should look the same for both
erquhart
erquhartβ€’9mo ago
Not only that, but useLoading appears to accept the return value of useQuery, whereas I think folks would expect useQuery to be wrapped and replaced. Pretty sure Ian already added a wrapper with that approach to the helpers for other purposes. Not at my machine right now and don't feel like dealing with GitHub's mobile UI lol
ian
ianβ€’9mo ago
For mutations there is no loading state except while you wait for the mutation to resolve, and it’s undefined in a hook since multiple mutations could be in flight. It also never returns undefined. I kept it composable so you can add other behavior too. And so if you have a useSessionQuery or other wrapper you don’t have to make a variant for every permutation. Another one to compose with it could be
const [ foo, isLoading] =useLoading(useQuery(api.bar.baz));
const [ fooOrStale, isStale ] = useStaleValue(foo);
const [ foo, isLoading] =useLoading(useQuery(api.bar.baz));
const [ fooOrStale, isStale ] = useStaleValue(foo);
But yeah I could make one that wraps it all for convenience in the vanilla case I’m not sure how to make a hook catch an error without a component for an error boundary - anyone have experience with that?
Abhishek
Abhishekβ€’9mo ago
Instead of throwing the error, can we return an object that indicates whether an error occurred, and the error itself? will this approach be good?
FleetAdmiralJakob πŸ—• πŸ—— πŸ—™
I think this is possible but not with an wrapper. To do that we have to change the convex/react code itself
ian
ianβ€’9mo ago
I think it is possible wrapping the useQueries API. I want to make this right now, but have a few other things on my plate today. What do you think of this API?
const {
data,
isLoading,
error,
} = useQuery(api.foo.bar | "skip", args);
const {
data,
isLoading,
error,
} = useQuery(api.foo.bar | "skip", args);
composed with things like:
const {
data,
isStale,
staleArgs,
} = useStaleValue({data});
const {
data,
isStale,
staleArgs,
} = useStaleValue({data});
where the latter will pass through extra fields, so you can do:
const {
data,
isStale,
isLoading,
error,
} = useStaleValue(useQuery(api.foo.bar | "skip", args));
const {
data,
isStale,
isLoading,
error,
} = useStaleValue(useQuery(api.foo.bar | "skip", args));
Module: react | Convex Developer Hub
Tools to integrate Convex into React applications.
ian
ianβ€’9mo ago
I'll probably hold off on doing it for usePaginatedQuery since that already has an isLoading variable returned, and since errors are trickier to reason about and would take some more thought (e.g. what if only one page errors - do you return partial data? what if many pages error, do you return all the errors?)
Abhishek
Abhishekβ€’9mo ago
I love this API pattern. This is exactly what I had in mind.
FleetAdmiralJakob πŸ—• πŸ—— πŸ—™
I agree with abhishek
Abhishek
Abhishekβ€’8mo ago
Hi @ian , any update on this ? Thank you
ian
ianβ€’8mo ago
It’s behind rate limiting in the queue
FleetAdmiralJakob πŸ—• πŸ—— πŸ—™
Interesting. I once asked a question about ratelimiting (https://discord.com/channels/1019350475847499849/1203007687344914503) What will this feature exactly be?
ian
ianβ€’8mo ago
More active thread on it: https://discord.com/channels/1019350475847499849/1230976450015006751 @Abhishek @FleetAdmiralJakob πŸ—• πŸ—— πŸ—™ I put a draft of it in convex-helpers@0.1.39-alpha.0 (not tested yet):
import { useQueryWithError } from "convex-helpers/react";
import { useQueryWithError } from "convex-helpers/react";
The PR is here. As I played with the types, however, there are a couple edge cases: 1. When you pass "skip" instead of arguments, it returns isLoading: true even though that's not quite true. I think this is generally what you'd want though. 2. When it returns error it returns isLoading: false which is true, but it means that this code doesn't type check:
const { data, isLoading } = useQueryWithError(api.foo, args);

if (!isLoading) console.log(data[0])
const { data, isLoading } = useQueryWithError(api.foo, args);

if (!isLoading) console.log(data[0])
Why not? because with the types, when isLoading is false then data could be undefined and error could be defined. So... I'd rather not lie about isLoading when an error is returned, but it would be more convenient for the default path... wdyt? I could also include other booleans and have:
{
data: <whatever your function returns>,
isPending: boolean,
isError: boolean,
error: Error,
status: "success" | "error" | "pending"
}
{
data: <whatever your function returns>,
isPending: boolean,
isError: boolean,
error: Error,
status: "success" | "error" | "pending"
}
so you could just check status === "success" in the "I only care about success" world.
ian
ianβ€’8mo ago
Ok I updated it in convex-helpers@0.1.39-alpha.1 to match the tanstack query syntax:
| {
status: "success";
data: FunctionReturnType<Query>;
error: undefined;
isSuccess: true;
isPending: false;
isError: false;
}
| {
status: "pending";
data: undefined;
error: undefined;
isSuccess: false;
isPending: true;
isError: false;
}
| {
status: "error";
data: undefined;
error: Error;
isSuccess: false;
isPending: false;
isError: true;
}
| {
status: "success";
data: FunctionReturnType<Query>;
error: undefined;
isSuccess: true;
isPending: false;
isError: false;
}
| {
status: "pending";
data: undefined;
error: undefined;
isSuccess: false;
isPending: true;
isError: false;
}
| {
status: "error";
data: undefined;
error: Error;
isSuccess: false;
isPending: false;
isError: true;
}
So you can check isSuccess or status === "success"
Queries | TanStack Query React Docs
Query Basics A query is a declarative dependency on an asynchronous source of data that is tied to a unique key. A query can be used with any Promise based method (including GET and POST methods) to fetch data from a server. If your method modifies data on the server, we recommend using Mutations instead.
erquhart
erquhartβ€’8mo ago
Amazing, thanks for this @ian
FleetAdmiralJakob πŸ—• πŸ—— πŸ—™
Wonderful, will check it out asap. Great work, thank you.
Abhishek
Abhishekβ€’8mo ago
@ian so good , will play with it and see how its working
Son
Sonβ€’8mo ago
This is awesome!
ian
ianβ€’7mo ago
I'll write up some tests and drop it into a project to make sure it behaves itself before publishing. I also think a post on how to do the types so folks can wrap useQuery / useMutation themselves would be helpful. Folks have been asking about this for automatically adding parameters, etc. But that might come after a revamp to the RLS helpers & some more work on making a headless API version of my llama farm project I fixed a bug and published it in 0.1.40.
import { useQuery } from "convex-helpers/react";

const { data, status, error } = useQuery(api.foo.bar, args);
import { useQuery } from "convex-helpers/react";

const { data, status, error } = useQuery(api.foo.bar, args);
Abhishek
Abhishekβ€’7mo ago
@ian can we use the same for useMutation too ?
ian
ianβ€’7mo ago
What do you mean, exactly? The API is a bit different in that: 1. It's returning a function that can be run multiple times, so there isn't a global "isRunning" state 2. The "error" state is tied to a specific call, not a global state, and unclear when it'd be "cleared" I think there was some previous discussion of this but can't remember what API sounded useful - e.g. isPending could be true if any mutation is in flight, and error state could be the most recent invocation?
Abhishek
Abhishekβ€’7mo ago
@ian
For point 1) Can we have something like const {mutateFn, isPending, error} = useMutation(api.myFunctions.mutateSomething); then use it like onClick = > mutateFn(arg1, arg2) {isPending} return <Loading> For ref - https://tanstack.com/query/latest/docs/framework/react/guides/mutations
Mutations | TanStack Query React Docs
Unlike queries, mutations are typically used to create/update/delete data or perform server side-effects. For this purpose, TanStack Query exports a useMutation hook. Here's an example of a mutation that adds a new todo to the server:
Abhishek
Abhishekβ€’7mo ago
And an error state tied to a specific call is a good thing. Then we can easily manage individual call errors differently, rather than a global state. And may we can change the hook name so it can be used for both mutation and action
ian
ianβ€’7mo ago
I'm not sure how you tie the error state to a specific call if it's returned from a hook. e.g.: const { error, mutate } = useMutation(api.messages.send); If you call mutate 3 times at once, which invocation of mutate is tied to that error object? They provide onSuccess etc. per-function callbacks, which seem equivalent to our promise-based mutate(args).catch(onError) since our mutations are always async
Abhishek
Abhishekβ€’7mo ago
@ian , So if we call mutate 3 times then the error obj should represent the global error state For example, if mutate 1 and mutate 2 are successful but we get error on mutate 3 then error should be true even if 2 mutates were successful ALSO if possible we can provide something like mutate1.error to represent individual errors.
const { error, mutate } = useMutation(api.messages.send);

mutate(data1);
mutate(data2);
mutate(data3);

if (error) {
console.error(error);
// Display an error message to the user
}
const { error, mutate } = useMutation(api.messages.send);

mutate(data1);
mutate(data2);
mutate(data3);

if (error) {
console.error(error);
// Display an error message to the user
}
if one of the mutations fails, the error object returned by the useMutation hook will contain the error object that was thrown for that mutation. The error object will not contain any information about the other mutations that succeeded. This is my opinion, if others agrees with same then we are good @ian what do you think about this approach?
ian
ianβ€’7mo ago
Some thoughts: 1. I don't like that it's not clear which mutation is associated with the error. If multiple fail for different reasons, you don't know what to communicate or what to retry. I'd prefer a per-invocation error, which is how the current API works. 2. I don't see how mutate1.error would work - I don't see how a hook could provide that 3. If the first call failed and the second succeeded I'd expect error to not be set - otherwise the UI would indicate an error when the second attempt succeeded. 4. This API only makes sense to me in the context where you can only have one call to the mutation in flight at once, which is maybe how TanStack mutation works. Overall I'm still too skeptical of the benefits to write an opinionated helper for it right now, but I'd encourage you to wrap useMutation yourself to iterate on the behavior you want and let me know if you land with something you think is broadly useful
Abhishek
Abhishekβ€’7mo ago
Actually @ian the things I wrote is similar to how use mutation of tanstack query works. Which is widely used by most dev but I understand your concern
ian
ianβ€’7mo ago
Yeah I believe you, I just don't understand how. I need to play around with the tanstack version and whether it only allows serial calls (though the docs say they'll run in parallel) or how it can provide the invariants around status that it claims (that it's only ever in one state). Until then the community will need to lead the way on it, but I can provide some type helpers and a post on how to wrap useQuery / useMutation / etc. to enable more wrappers to be written by the community, which collectively has much more experience and diversity of ideas than I do.
Abhishek
Abhishekβ€’7mo ago
Yes the post will be helpful so people can create their own wrapper depending on their use cases
adam
adamβ€’7mo ago
The useQuery() wrapper seems useful. I actually don't find much value in the useMutation() hook from convex/react and don't tend to use it. Let me show my alternative approach and why.
function App() {
const convex = useConvex(); // <= I use this later for mutations
const messages = useQuery(api.messages.list);

const handleFormSubmit = async (message: Message) => {
await convex.mutation(api.messages.send, message);
};
}
function App() {
const convex = useConvex(); // <= I use this later for mutations
const messages = useQuery(api.messages.list);

const handleFormSubmit = async (message: Message) => {
await convex.mutation(api.messages.send, message);
};
}
I prefer this approach because it keeps the related code in one place and I can easily Command + click to jump to the backend function definition for api.messages.send. There is also less hook definitions at the top of my file if I have multiple mutations in the component. I thought I would mention this as it might stimulate peoples thinking around how a useMutations() wrapper might behave.
ballingt
ballingtβ€’7mo ago
Yeah I skip useMutation sometimes too. It does provide a memoized function if you need to pass that as a prop and is the API for optimistic updates, but often I don't need these.
ian
ianβ€’7mo ago
Good call - I hadn't thought of that but I do like organizing things that way, instead of jumping to where the useMutation was defined. Thanks for the tip
erquhart
erquhartβ€’7mo ago
Thought I'd provide a common use case for the useMutation() helper: - Mutations trigger API calls - I want to avoid excess API calls by blocking if we're already waiting for a response - Currently I have to track state on my own to do this - If useMutation returns a loading state, I can just use that As it stands, I would need to do this manually for almost every mutation in my app. Regarding this not making sense because you don't know which call you would be tracking, it's up to the user to run the hook multiple times for multiple calls:
const { status: statusA } = useMutation(api.my.mutation)
const { status: statusB } = useMutation(api.my.mutation)
const { status: statusA } = useMutation(api.my.mutation)
const { status: statusB } = useMutation(api.my.mutation)
To be fair, though, for Convex, optimistic updates will probably be used instead for many of these call sites
sshader
sshaderβ€’7mo ago
I want to avoid excess API calls by blocking if we're already waiting for a response
If you're using the ConvexReactClient, mutations will execute in order. Or is this about preventing the user from enqueueing a bunch of redundant mutations (e.g. disabling a button in the UI while a particular mutation is in progress)?
ian
ianβ€’7mo ago
If you want to drop intermediate requests, you can check out the single-flight helpers: https://stack.convex.dev/throttling-requests-by-single-flighting
Throttling Requests by Single-Flighting
For write-heavy applications, use single flighting to dynamically throttle requests. See how we implement this with React hooks for Convex.
Abhishek
Abhishekβ€’7mo ago
Even I am doing the same for every mutation Yes disabling the button
erquhart
erquhartβ€’7mo ago
The callback doesn't work for this case (as it would still execute the pending calls once the blocking call resolves), but useLatestValue() should work. That said, setting that hook up for each mutation is the same kind of boilerplate as just tracking the pending state of the mutation manually with useState(). We could, of course, wrap this in custom version of useMutation to avoid the boilerplate, but that brings us back to just returning the tanstack style object from useMutation. I didn't look deep into the callback, maybe there is a way to have it drop instead of execute after the current request, but it's still an extra step we're taking for every mutation that we don't want to allow the UI to fire repeatedly. Any of these solutions at scale will end up in a custom useMutation(). @ian but again, to your original points around the confusion of returning a state/error object from useMutation(), it's trivial for the user to run the hook multiple times to track multiple call sites if and when they need that. In most cases they won't. Note that multiple runs of the same hook do not need to have awareness of one another at all.
// Most use cases won't need this, but if they do:
const { status: statusA, error: errorA } = useMutation(api.my.mutation)
const { status: statusB, error: errorB } = useMutation(api.my.mutation)
// Most use cases won't need this, but if they do:
const { status: statusA, error: errorA } = useMutation(api.my.mutation)
const { status: statusB, error: errorB } = useMutation(api.my.mutation)
Not pushing that this needs to be done, but wanted to at least clear up the question marks.
ian
ianβ€’7mo ago
it's trivial for the user to run the hook multiple times to track multiple call sites if and when they need that.
Remember that useMutation doesn't call it immediately - it returns a function you can call N times asynchronously - e.g. on keyDown and you have to have a fixed number of hooks for React to work, so you'd have to know how many mutations you were planning to call at once
erquhart
erquhartβ€’7mo ago
That's the case for tanstack as well, useMutation() object also includes a mutate method (should have included that in the example). In most cases you won't have multiple call sites within a component for the same mutation, but if you do, you would use a dedicated useMutation() for each one that you want to track status for. Any call sites that don't require status updates can just share the same hook.
// for call sites that don't care about status
const { mutate } = useMutation(api.my.mutation)

// for two specific call sites that do track status
const { mutate: mutateA, status: statusA, error: errorA } = useMutation(api.my.mutation)
const { mutate: mutateB, status: statusB, error: errorB } = useMutation(api.my.mutation)
// for call sites that don't care about status
const { mutate } = useMutation(api.my.mutation)

// for two specific call sites that do track status
const { mutate: mutateA, status: statusA, error: errorA } = useMutation(api.my.mutation)
const { mutate: mutateB, status: statusB, error: errorB } = useMutation(api.my.mutation)
ian
ianβ€’7mo ago
but what happens if mutateA is called 5 times before the first one returns?
<button onClick={() => mutateA()}>like</button>
<button onClick={() => mutateA()}>like</button>
erquhart
erquhartβ€’7mo ago
<button onClick={() => {
if (statusA !== 'pending') {
mutateA()
}
}}>like</button>
<button onClick={() => {
if (statusA !== 'pending') {
mutateA()
}
}}>like</button>
That's the problem this is meant to solve You could have a couple of different places in the UI that care about the status of an individual mutation, so it's nice to have it available at the top of the component scope.
Abhishek
Abhishekβ€’7mo ago
This is off the topic but will it be possible to make the convex function call to support the tanstack query lib out of the box ? If not what is stopping us from doing this ?
jamwt
jamwtβ€’7mo ago
We’re actually chatting with Tanner about this very idea right now. It may come together.
Abhishek
Abhishekβ€’7mo ago
@jamwt let's goo
ian
ianβ€’7mo ago
One thing is that the API there is to call an async function for the query, not something with a subscription by default. so the queryFn argument type seems like a non-starter? I guess my question is: if you don't have this check (or if the React render doesn't update the captured value of statusA in time to disable the button), then how does a single statusA relate to the 2+ invocations happening? My gut is that you'd want it to just reflect the latest call - but you'd always lose some information (e.g. if one call fails, another succeeds, then another is pending). I get that you can have a policy of only calling it one at a time, but if I were to expose an API like that I'd want well defined behavior if you run it multiple times.
erquhart
erquhartβ€’7mo ago
Tanstack query useMutation reflects the last invocation status/error: https://tanstack.com/query/latest/docs/framework/react/reference/useMutation That makes sense to me, and is likely what folks will expect
hasanaktasTR
hasanaktasTRβ€’7mo ago
The new useQuery hook has made our job much easier. Now we can easily throw errors from APIs, we do not need to build our own special structure. Previously, whenever we threw an error, it would go into the React Error Boundaries and often blow up the entire application. I also agree with the useMutation suggestions. It would be great if we could fully reflect the logic in the Tanstack query. As for the use case, in addition to the above. For example, when an error is returned from a mutation, I want to automatically open a dialog box and print a message in it. If a field like isError is returned from the UseMutation hook, I open the automatic dialog. When I say close the dialog, I use the reset function returned from useMutation. Scenarios like this can be derived. These can be added to useMutation instead of managing them in a separate state. We use react query in the projects we manage at the company I work for, and useMutation makes our job much easier.
jamwt
jamwtβ€’7mo ago
I think I've lost track of what we're discussing here. is there a semantic that's fundamentally missing in convex APIs? i.e., that @ian wasn't able to provide with his tanstack-query-like wrapper?
ian
ianβ€’7mo ago
Interesting. By my read, for pending it's if any request is in flight (even if the latest succeeded), and error/success for the latest, and will return data for the last success, so if the latest failed and other is in flight, data will still be there (stale in Convex terms). Thanks for nudging me through this all. I have some other priorities rn, but feels like enough well-defined behavior that I understand the path forward. Anyone want to take a stab at it before then? Wrapping useMutation shouldn't be too hard. here's a starter:
export function useMyMutation<
Mutation extends FunctionReference<"mutation", "public">,
>(name: Mutation) {
const originalMutation = useMutation(name);

return useCallback(
async (
...args: OptionalRestArgs<Mutation>
) => {
const data = await originalMutation(...args);
return {
data
}
},
[originalMutation],
);
}
export function useMyMutation<
Mutation extends FunctionReference<"mutation", "public">,
>(name: Mutation) {
const originalMutation = useMutation(name);

return useCallback(
async (
...args: OptionalRestArgs<Mutation>
) => {
const data = await originalMutation(...args);
return {
data
}
},
[originalMutation],
);
}
erquhart
erquhartβ€’7mo ago
useQuery is great, the discussion is now about the merits and requirements for a useMutation counterpart.
ian
ianβ€’7mo ago
Re: the query API, we provided the same return types as the TanStack useQuery but the function arguments queryKey and queryFn would be different types
jamwt
jamwtβ€’7mo ago
ah, sorry, I thought convex-helpers included useMutation already my bad
hasanaktasTR
hasanaktasTRβ€’7mo ago
https://gist.github.com/hasanaktas/ffa0a6afc0d02135281b3d66aa024653 I created a simple example. If you want me to develop it for convex-helpers, I can create a more detailed function. I couldn't share it via discord due to the 2000 character limit.
Gist
convex extended use-mutation
convex extended use-mutation. GitHub Gist: instantly share code, notes, and snippets.
ian
ianβ€’7mo ago
hell yeah! Want to put in in #show-and-tell for visibility? One change I'd make is to have an integer in a useRef that you bump every time you start a request, and only update the success status if the useRef.current value matches the one you started with - that way you only update it if you're the latest request, same for error
hasanaktasTR
hasanaktasTRβ€’7mo ago
I added the gist as you said and made the code cleaner. Maybe something can be done for the error type, but overall this hook met all my needs. Since I am using React 19 and the new experimental react compiler, I do not use the useCallback and useMemo hooks. Maybe the person who will use it may want to add these.
Abhishek
Abhishekβ€’7mo ago
Sorry I'm lost with all the discussion so in conclusion we can try @hasanaktasTR implementation of use mutation or @ian you're working on a official one ? Or we can wait for tanstack query to support conversation function ? @jamwt Thanks
ian
ianβ€’7mo ago
For now I'd use this implementation and let us know your feedback. Hopefully a future change to move it into helpers would just be an import change.
faluyiHype
faluyiHypeβ€’6mo ago
I appreciate the work that the convex team is doing, but honestly not sure why this behavior was absent from the start? It seems like a very common use case to want to know the status of an async function and have your UI reflect that. Can anyone from the team explain why this wasn't supported natively and how they originally imagined us handling this behavior especially on devices with slow network connections.
erquhart
erquhartβ€’6mo ago
No new functionality was added here, the tanstack style api is just syntactic sugar. It's always been possible to track the status of a mutation via the promise returned by the mutation function.
faluyiHype
faluyiHypeβ€’6mo ago
Yeah I know, just wondering why it wasn’t a default in the hook. It’s easy to add on your own but might have been a nice to have
erquhart
erquhartβ€’6mo ago
Yeah, definitely a fair sentiment. This response from Jamie to a similar question is a good summary on why: https://discord.com/channels/1019350475847499849/1256428053127495690/1256657363310743622 Crazy number of potential nice to haves and valuable features that could be added. I honestly believe (in most cases) there is no problem for a startup that is objectively more difficult than prioritization. As we all know, it involves saying "no" a lot. And I haven't seen a team do a better job at this than Convex. (And they've said "no" or "not yet" to some things I really want!)
faluyiHype
faluyiHypeβ€’6mo ago
Yeah for sure. I’m excited to see what else they cook up.
erquhart
erquhartβ€’6mo ago
I know you specifically asked for someone from the team to respond, and they probably still will, just slower on the weekend
faluyiHype
faluyiHypeβ€’6mo ago
Yeah I just wanted to make sure there wasn’t a mental model that I was missing when thinking about convex because I didn’t see slow or offline connections addressed in demos or talks
erquhart
erquhartβ€’6mo ago
ian
ianβ€’6mo ago
Yeah erquhart covered it, but to echo: you already have all the information, just in a different form. The mutation promise won't resolve/reject until it has been committed and the results are showing up in queries. The optimistic update API allows you to make local changes until that happens, but I personally just use a useState hook or otherwise to temporarily capture state. As I mention above, having a single status for a hook that returns a function that can be called N times requires making assumptions that can lose information. While you can try to design your usage to avoid confusing behavior, Convex generally provides smaller rock-solid primitives that you can use to build the higher level ergonomics you want, rather than a more opinionated hook with less-well-defined behavior. We actually are working on a way to work with the TanStack ecosystem more directly. https://docs.convex.dev/client/tanstack-query
Convex with TanStack Query | Convex Developer Hub
If you already use TanStack Query the query
Son
Sonβ€’6mo ago
Since you’ve created a helper function for managing queries in a similar way, in your estimation is worth while switching over to this. Im using convex in a react-naive project .
ian
ianβ€’6mo ago
I wouldn't spend time migrating away to use it. If there's a feature you want there, you can selectively use it alongside your current queries. But I personally just use vanilla convex useQuery / useMutation as the default. If you already were using TanStack for other things (like TanStack Router) or make a new web project with TanStack Start, then it comes with other benefits, like making preloading a route server-side easier. But I don't think that translates to React Native currently.
Son
Sonβ€’6mo ago
Makes alot of sense, thanks for the confirmation

Did you find this page helpful?