Closed utterances-bot closed 2 years ago
Another great post! You're on π₯, congrats!
I have a question about some questions about this topic:
No, I donβt think itβs a bad practice. Personally, I try to keep the mutation as close to where itβs needed (e.g. the button that performs it) and I usually donβt need the result anywhere else, because the mutation influences associated queries, and I use those everywhere :)
thanks for awesome post! i have a question @TkDodo,
i prefer use mutateAsync
with unhandled promise rejection for handling unknown, unexpected error
so, my question is...
mutate
?@JaeYeopHan For global error handling, I would set an onError
event handler on the MutationCache
. It will be executed once for every error that occurs for any mutation. I'm describing this here: https://tkdodo.eu/blog/react-query-error-handling#the-global-callbacks
@TkDodo oh thanks! :)
onError event handler on the MutationCache
that's the part I missed.
may i ask you what is the difference between unhandled promise rejection handler
and MutationCache onError handler
?
@JaeYeopHan unhandled promise rejections occur when the browser sees a failed Promise without a .catch
anywhere. If you use .mutate
, react-query will catch it for you so that can't happen. The global onError callbacks are just additional functions that are invoked when an error occurs.
Awesome post, thanks for sharing! Returning a Promise
from onSuccess
has solved some UI details in a current project I'm working on :tada:
@TKDoDo Thanks for the awesome article, I guess I have been doing mutations all wrong!
A common pattern I use is having multiple custom hooks that internally call useMutation
and handle the cache invalidation / optimistic updates. Typically a user goes through a multi step form and we need to update / create multiple resources.
I always used mutateAsync so I could await, get the result and then call the next mutation. How would you solve a typical case like that?
I always used mutateAsync so I could await, get the result and then call the next mutation. How would you solve a typical case like that?
Dependent mutations can basically be solved in 3 different ways. It's a pity I didn't get to include that in the article, I kinda forgot about it π :
1) make multiple calls inside the mutateFn:
const mutate = useMutation((data) =>
axios.post('/something', { data })
.then((somethingResult) =>
axios.put('/somethingElse', { somethingResult } )
)
)
<button onClick={() => mutate('data') />
advantage would be one mutation with one loading state. disadvantage would be you can't easily trigger them separately if you'd need to.
2) with mutateAsync
:
const mutate1 = useMutation((data) => axios.post('/something', { data }))
const mutate2 = useMutation(somethingResult) => axios.put('/somethingElse', { somethingResult })
<button onClick={async () => {
try {
const somethingResult = await mutate1.muteateAsync('data')
const result = await mutate2.mutateAsync(somethingResult)
} catch {}
}}/>
a bit boilerplate-y and you'd need to combine loading states very likely
3) like 2, but with mutate and callbacks:
const mutate1 = useMutation((data) => axios.post('/something', { data }))
const mutate2 = useMutation(somethingResult) => axios.put('/somethingElse', { somethingResult })
<button onClick={() => {
mutate1.mutate('data', {
onSuccess: mutate2.mutate
})
}}/>
separated, but still quite concise. will get messy with more than 2 mutations, and the end-result would only be available in the onSuccess callback of the second mutation if you need it
Hi, thanks I figured it will be one of those 3, still torn on what should be used best. ATM we use option 2 (without the try / catch woopsie), but we also so a couple of issues with toasters flying in too early and page redirects to newly created resources not always working (cache is being updated but page is already redirecting)
The last option also seems a bit messy but I understand the reasoning. What I'm thinking now is having 3 independent useMutation hook wrappers and then wrap those in a component specific hook also using useMutation, not sure how that will pan out but going to try that. The benefit would be that I still can call the hooks independently but I can also call them in one go.
Thanks for the reply!
how can I mock useIsMutating hook with msw or jest for fetching state of particular mutationKey?
@hedaukartik if you use msw
, you'd just mock the network request for the given mutation and useIsMutating
would just return the correct value, because the request is "in-flight" as far as react-query is concerned. That's the beauty of mocking on that level :)
@TkDodo my use-case was to show loader in different component and not in the component where my useMutation
is called. useIsMutating
helped me in same.
While testing if loader is shown on different component..in my RTL test case of the component..i triggered useMutation
wrapper hook using renderHook
provided by code>@testing-library/react-hooks</code and with waitFor I was able to see loader in test as well.
In the docs, const isMutatingPosts = useIsMutating(['posts'])
this is working for me on any mutation happening. Even if I add 'postsssss' or 'pooosts', it goes in fetching state. This worked for me- const isMutatingPosts = useIsMutating({ mutationKey: 'posts'})
if mutationKey is 'posts' in useMutation
.
PS. MSW is smooth with react-query. Thanks for your amazing articles out there.
Beautiful article, enjoyed all the way through including comments.
Hey, many thanks for your awesome articles! They really helped me a lot to learn react-query more deeply! Unfortunately I'm kinda stuck here with a mutation and I can't figure out how to make this work, if some of you guys had already this scenario and could give some hints it would help me a lot.
So I have built my mutation:
export const useAddPeer = (
portfolioId: PortfolioId,
): UseMutationResult<PortfolioWithPeers, unknown, PeerRequestBody, unknown> => {
const queryClient = useQueryClient();
return useMutation(
(newPeer: PeerRequestBody) =>
handleRequest<PortfolioResponse, AddPeersIntoPortfolioRequestBody>({
url: `${API_PATH}/${portfolioId}/peers`,
method: 'PUT',
payload: {
peers: [newPeer],
},
}),
{
onSuccess: () => queryClient.invalidateQueries('portfolio-overview'),
},
);
};
And here is how I call it:
const addPeer = useAddPeer(activePortfolio.id);
const handleAddPeer = (newPeer: PeerRequestBody) => () => addPeer.mutate(newPeer);
<Box
onClick={handleAddPeer({
tickerId: ticker.id,
isFavorite: false,
})}
> ... </Box>
and my PUT
request works fine, but my mutation never happens (and I went into onSuccess
with the debugger) but as far as I saw, I can just see one query key there, but not the one I'm trying to mutate 'portfolio-overview'
.
And here is how my portfolio-overview
query looks:
export const usePortfolioOverview = (
portfolioId: PortfolioId,
): UseQueryResult<PortfolioOverviewResponse, Error> =>
useQuery<PortfolioOverviewResponse, Error>(
['portfolio-overview', portfolioId],
() =>
handleRequest({
url: `${API_PATH}/${portfolioId}/overview`,
method: 'GET',
}),
{
enabled: !!portfolioId,
},
);
basically the logic is -> I'm inside another component which I search for the "peers" and after clicking on it, it's added to the portfolio, which then maps all peers and display all of them in a list, and that's what I'm trying here, to mutate this list after adding a new peer to it.
@RSchneider94 I don't see anything wrong with that code on first sight. If you can reproduce your issue in codesandbox, it would be easier to analyze :)
@RSchneider94 I don't see anything wrong with that code on first sight. If you can reproduce your issue in codesandbox, it would be easier to analyze :)
hey @TkDodo ! thanks for your reply! sure, I have created quickly here a Sandbox that the pseudocode has the logic almost the same as mine (of course simpler): https://codesandbox.io/s/lucid-architecture-pny2l
thanks!
@RSchneider94 I've answered on the react-query discussion: https://github.com/tannerlinsley/react-query/discussions/2820
All articles were big help to me. thank you π₯Ί If you don't mind, can I translate the writings into my native, Korean, and post them on my blog? (of course, including the source)
mutation retries (if the device is offline): 1) will it work if i just mention retries as some number like 3
const mutation = useMutation(addTodo, {
retry: 3,
})
2) or i will have to persist the mutation as specified in the docs:
// If the mutation has been paused because the device is for example offline,
// Then the paused mutation can be dehydrated when the application quits:
const state = dehydrate(queryClient)
3) in the above case (2) do i have to store state in the LOCAL storage and then hydrate it when application is connected again?
// The mutation can then be hydrated again when the application is started:
hydrate(queryClient, state)
// Resume the paused mutations:
queryClient.resumePausedMutations()
4) I am using persistQueryClient with createAsyncStoragePersistor Do i need to do any of the above if i am using this setup? If yes/no then how query retry will work?
@seung-00 Definitely, I'd appreciate translations. Please reach out once you have them, I will link them at the top of the blog. Ideally, I'll do something similar to what Kent C. Dodds has: https://github.com/kentcdodds/kentcdodds.com/blob/main/CONTRIBUTING.md#translation-contributions
@sushilbansal mutation retries is probably worth a blog post on its own, and I haven't really played around with it much. From what I know right now, I'd say:
queryClient.resumePausedMutations()
Thanks for this great article, Dominik! I understand mutations are generally for updating data on the server, but what about the times when you need to fetch data imperatively and maintaining a cache for it is unnecessary?
For example, when a user clicks a button to upload a file, I need to check whether the file has already been uploaded to the server. Right now I'm just awaiting the response from mutateAsyc
in the click event callback and it works fine. Just curious whether you think that's an anti-pattern because I'm just reading data from the server, not changing it. I know I could just make the request without react-query, but I'd like to keep the option to easily configure retries, a loading state, etc.
@nrbernard I think this should be fine. One disadvantage is that you still need to join loading / error states together, because it sounds like you have two mutations: one to check if the file has already been uploaded, and then one to actually do the file upload.
Another solution would be to just make two requests inside the one file upload mutation. You can chain promises in there, too, and you will only have one loading / error state. Unless you need individual retries, this should work (pseudo code):
useMutation(async (file) => {
const exists = await checkIfFileExists(file)
return exists ? file : uploadFile(file)
})
Thanks for the great post!
Can React Query help optimistic updates with the following traits?
It gets into the realm of client state instead of server state, and I think one can claim that this is true for all optimistic updates (it's client state and not server state, and hence its place is not in a server-state library like React Query).
Thanks again!
@bartzy
ad 1): definitely. you can append the item to your list in the queryCache, and somehow mark it as temporary (a negative id or in some other form) and then render it e.g. greyed out or so in the ui.
ad 2): when an optimistic update fails, the usual approach is to immediately rollback to the previous state and then re-fetch to get the real server state. But you could also keep the entry in the list and let the user retry it, sure. You'd need to mark it as failed
in the onError
callback.
Hi, thanks for the great library... I am having tiny bit of oddness where on a mutation the invalidation is being fired before the api call to the server. In other words, it is querying the server prior to making the changes on the server...
const updateOrder = useMutation<
ItemType,
Error,
{ newOrder: string[]; categoryId: string }
>(
({ newOrder, categoryId }) =>
updateMenuOrder(projectId, thingId, categoryId, newOrder),
{
onSuccess: () => {
queryClient.invalidateQueries(['thing', { id: thingId }]);
},
},
);
the mutation is being used in conjunction with react-beautiful-dnd
and triggered on drag end to update the sort order:
const onDragEnd = result => {
const { destination, source, draggableId } = result;
setDraggingFromCategoryId('');
if (!destination) {
return;
}
if (
destination.droppableId !== source.droppableId ||
destination.index === source.index
) {
return;
}
const newOrder = Array.from(
thing[source.droppableId].items,
);
newOrder.splice(source.index, 1);
newOrder.splice(destination.index, 0, draggableId.split('|')[1]);
updateOrder.mutate({
newOrder: newOrder,
categoryId: source.droppableId,
});
return;
};
I cannot seem to figure out why
tried setQueryData instead of invalidating and still no joy... I see it making the call tot there server and returning the correct data, but for some reason it is not updating. even tried to do an optimistic update using the onMutate... this is very odd behavior
ok, solved the issue with onMutate to do an optimistic update, still odd that the invalidate was being fired before the server update
@incognos best explanation for me would be that the server returns the response before having actually finished the mutation.
Hey, thanks a lot for this amazing article... I have a question, can you have some example to unit tests using the setQueryData() inside the mudation to update the cached data? I am really struggling with that π
Very nice article and an excellent library In the example:
const useUpdateTodo = () =>
useMutation(updateTodo, {
// β
always invalidate the todo list
onSuccess: () => {
queryClient.invalidateQueries(['todos', 'list'])
},
})
es-lint raises this error https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/no-floating-promises.md
Is it a good practise to ignore the above rule as I have to add return
and wait for invalidate to complete :(. Also when we have multiple queries to be invalidated I need to wrap them in Promise.all
and return. Does this look ok. Can it be improved
const useUpdateTodo = () =>
useMutation(updateTodo, {
// β
always invalidate the todo list
onSuccess: () => {
return Promise.all([
queryClient.invalidateQueries(['todos', 'list']),
queryClient.invalidateQueries(['reminders'])
]);
},
})
do we see any problem in the above pattern. Bottom line, it seems, we cannot use the below pattern without disabling eslint no-floating-promises rule.
// π fire and forget - will not wait
onSuccess: () => {
queryClient.invalidateQueries(['posts', id, 'comments'])
}
@NehalDamania as outlined, react-query will behave differently if you return the Promise, as it will await it. So your mutation will stay in loading state "longer". That might be on purpose, but it might be.
I like the eslint rule because it makes you aware that you are ignoring a floating promise, which could be a mistake, or it could be on purpose. To make it explicit that this is intended, use the void
operator:
// π fire and forget - will not wait
onSuccess: () => {
void queryClient.invalidateQueries(['posts', id, 'comments'])
}
@jennifertakagi I would guess your struggles come from mocking your custom useMutation
hook, in which case the callbacks won't be called. If you stick to mocking the network layer as advised in Testing React Query, the callbacks would be executed just fine (like in the application).
Do you have any suggestions where you need to fire off a dynamic amount of mutate calls and would like to track the isLoading state of the group? Generally we would use a Promise.all().
Does React-Query have anything out of the box for this?
My scenario is for a multiple file upload screen.
Do you have any suggestions where you need to fire off a dynamic amount of mutate calls and would like to track the isLoading state of the group? Generally we would use a Promise.all().
Does React-Query have anything out of the box for this?
My scenario is for a multiple file upload screen.
This was my question posted from your site; I do not know how it linked me with a different user. Appears to be some issue with ulterances extension.
I would also like to add that I want to perform an action once the mutations have all finished.
@nathanhannig There was an issue with utterances, which was fixed some days ago. Maybe the script is still cached, I'll try to force a refresh with a query param. sorry for that.
As for the question: We currently do that manually, and track the amount of outgoing requests with a ref, then do something when the counter hits zero. It's not out of the box, but the case is not that common. Something like:
export const useTrackParallelMutations = () => {
const mutationNumber = React.useRef(0)
return {
startOne: () => {
mutationNumber.current += 1
},
endOne: () => {
if (mutationNumber.current > 0) {
mutationNumber.current -= 1
}
},
allEnded: mutationNumber.current === 0,
}
}
You can call startOne
in onMutate
, endOne
in onSettled
and then check for allEnded
.
Alternatively, you can map over your mutations and create a custom component. That can then have the useMutation
call and fire in an effect. If you give it a mutationKey, you can useIsMutating
to track the amount of outgoing requests. Also not great, but again, multiple concurrent dynamic mutations is not a case that comes up a lot. In most cases, the apis would support one request to update multiple things :)
For the dynamic loading state of the group, useIsMutating
+ a mutationKey
is definitely the way to go.
Thanks for the great article! I have a question. If I want to pass in parameters when I am using useMutation as a custom hook, how can I pass in parameters?
ex)
export const useSetResearch = () => {
// api call
return useMutation(({token, data}: ParamTypes) => setResearch(token, data),
{
onSuccess: () => {
console.log('success')
},
onError: () => {
console.log('fail')
}
}
)
}
// on the component
const {mutate} = useSetResearch()
const submitHandler = () => {
mutate({token: userToken, data: someData},
{
onSuccess: () => {
console.log('success')
}
}
)
}
am I using it right?
@ekankr2 you can pass in parameters either to the custom hook itself, or to the mutate function. I like to mix and match the two depending on use-case:
.mutate
for dynamic thingsfor example, when updating user info, I often bind the mutation itself to the id, but the actual data comes from the form input, so I pass it to mutate:
const updateUser = ({ id, name }) => axios.post(...)
const useUpdateUser = (id) => useMutation(name => updateUser({ id, name }))
function UserComponent({ userId }) {
const userMutation = useUpdateUser(userId)
<MyForm onSubmit={(name) => userMutation.mutate(name)} />
}
Hello Dominik, is there a way from the mutation configuration to not trigger a mutation while there's another one in progress already.
Take this as example, I don't see way of handling this in the from the mutation definition itself and I am repeating logic in a lot of components that are consuming that mutation.
const useUpdate = () => useMutation(postData)
function MyComponent() {
const { mutate: update } = useUpdate()
return <button onClick={update}>Update Data</button>
}
const useUpdate = () => useMutation(postData)
function MyComponent() {
const { mutate: update, isLoading } = useUpdate()
const handleUpdate = () => {
if (isLoading) {
return
}
update()
}
return <button onClick={handleUpdate}>Update Data</button>
}
@bustamantedev no, I don't think there is a better way. If multiple mutations are involved, useIsMutating
can help. But also, it's usually better to disable the button while the mutation is in loading
state. That needs to be done manually anyways, and it's arguably the better ux over a button that can be clicked multiple times, but doesn't do anything when clicked the second time. We often disable buttons + add a loading spinner to them, and I've seen other products do this as well.
Hi :) I am really new to React Query and I have a question.
I want to fetch an GET method api that validates user's phone number when they click the button. The action should only happen on clicks. In my opinion, even though it is a GET method, it would be more appropriate to use a mutation since it should only happen on click. However, I'm not sure because I couldn't find a definitive answer to this. Could you give me some advice to this?
Thanks π
@yehdori if you don't need to cache the state / share it between multiple components, and you don't need automatic fetching / smart refetching in any way, a mutation is fine. It basically just handles isError / isLoading states for you :)
Mastering Mutations in React Query | TkDodo's blog
Learn all about the concept of performing side effects on the server with React Query.
https://tkdodo.eu/blog/mastering-mutations-in-react-query?utterances=22c272992acbd295c16b127dAie0B3x1NqhQorEKRGvFH71aM5MGXVEjl9WD5QZk2FjzX1Df%2FkFTH3OXs%2BinSDdYvbJn6fzf77VJgjGGyG9yk2I3qyKrT3CxpNink9ZEvoWXCAFbJKwKTpAZCv4%3D