T
TanStack•4y ago
sensitive-blue

Error boundary for unmounted mutation or what else to use (v3)

Hi! 👋 I'm trying to solve a very specific use case and I need some help figuring out if it's even possible. I have a list of items rendered in React and each item has a dropdown. Inside the dropdown, there are actions and each of them is a mutation. If the mutation fails, the error message should be displayed below the item (or rather inside it but at the bottom). And we currently solved it by having all of the necessary useMutations in the item component. This means they need to be always initialised even if the dropdown is never opened. Moreover, some actions can be unavailable for some types of items, making it even less useful to have 5+ mutations on the root component just to be able to fire them from a dropdown. I was exploring different options to solve the problem. At first, I wanted to use simple stuff. There is useIsMutating and I could key my mutations by the action + the ID of the item. But it only works for pending states, not for errors. Next thing I did is that I resorted to MutationCache.subscribe to handle it in user land, but if there are 50 items on the screen, it has to create 50 subscriptions and do the filtering. It does work, but it is not nice DX. I then had the idea of trying useErrorBoundary for the mutations, but the problem is that as they are in the dropdown, by the time the mutation fails, the dropdown is already closed, so the error boundary is not triggered. What other ideas are there? Is there something in v4 that would make this simple? At the moment, we're still in v3 but something like this would make it worth to immediately upgrade. Thanks for any help
8 Replies
sensitive-blue
sensitive-blueOP•4y ago
Hey, by any chance, does anybody have any idea how to do it better?
other-emerald
other-emerald•4y ago
Every item should have its own useMutation and thus its own error state. What's the problem with that approach?
sensitive-blue
sensitive-blueOP•4y ago
The problem is mainly performance. When I have 50 items with 5 different mutation in a dropdown, it's 250 * useMutation() on mount. But they can only ever be used from a dropdown, so naturally, I'd want to only have useMutation() in the component in the dropdown. The item itself should only render errors, so it needs something like useOnMutationError(mutationKey, callback) like useIsMutating(mutationKey) . In the screenshot, this is an example of an item - a comment in the system. It has a dropdown in the right bottom corner and in that dropdown, there are actions like "delete", "pin to top" and so on. When using the action, it closes the dropdown, so errors can be handled in the dropdown, they have to be handled be the item itself, example in second screenshot (this is the error boundary being triggered). Now, knowing this, I would have to put all useMutations from the dropdown on the item (comment) component even if they are unavailable (eg. pin to top is only valid for TikTok comments and not Facebook comments and I can't delete a direct message, only comments) - again, another waste on mount. At the moment, I have arrived into an error "catcher" of this kind:
useEffect(() => {
return queryClient.getMutationCache().subscribe(({ options, state }) => {
if (!Array.isArray(options.mutationKey)) return

const [contentId, action] = options.mutationKey
if (contentId !== id) return

setIsLoading(state.status === 'loading')

if (state.status === 'error') {
setError({
error: Object.values(state.error.errors)[0],
action,
})
} else {
setError(null)
}
})
}, [queryClient, id])
useEffect(() => {
return queryClient.getMutationCache().subscribe(({ options, state }) => {
if (!Array.isArray(options.mutationKey)) return

const [contentId, action] = options.mutationKey
if (contentId !== id) return

setIsLoading(state.status === 'loading')

if (state.status === 'error') {
setError({
error: Object.values(state.error.errors)[0],
action,
})
} else {
setError(null)
}
})
}, [queryClient, id])
It's very verbose as I need to manually subscribe and filter by mutation key, this is basically a useOnMutationError(mutationKey, callback) custom hook but again, it feels strange and to trigger an error boundary I then have to manually throw.
No description
No description
sensitive-blue
sensitive-blueOP•4y ago
Or am I going too deep into the idea of "useless useMutation = too costly"? even if there are 250 of them on mount? The error in the screenshot is a generic one when Object.values(state.error.errors)[0] is unrecognized, other errors do not stop the comment from displaying...
other-emerald
other-emerald•4y ago
do you have measured performance problems? calling useMutation doesn't really do anything - the observer is only created when .mutate() is called ...
sensitive-blue
sensitive-blueOP•4y ago
Now, that's a very good question and I should have totally thought about measuring it. (for the record, we found measurable difference of removing 2 use effects because of the * 50 multiplier, it ended up as 100 effects removed, so I only expected useMutation to do something worth reducing)
other-emerald
other-emerald•4y ago
It doesn't do anything 😅 I think I was wrong - the observer is created immediately upon calling useMutation 🤔
sensitive-blue
sensitive-blueOP•4y ago
However, that is inevitable even to listen for errors and loading state, no? So far, I'm probably going for context/props - each mutation can be in the dropdown, and just propagate errors up either via context or via an onError passed through props to set a local state field. Having that on the root item is probably going to be cheeper than a couple of unused mutations at the root. The loading will rely on useIsMutating() but one of those should still be less than multiple mutations. I'm gonna have some numbers next week where I plan to do some profiling.

Did you find this page helpful?