Should manually updating the cache always be the preferred option after mutations as long as I get proper data from server? - reactjs

I am writing a CRUD app with React Query and I created some custom hooks as described here: https://react-query.tanstack.com/examples/custom-hooks
In the docs I see that there are basically two ways to update the cache after a mutation:
Query invalidation (https://react-query.tanstack.com/guides/query-invalidation)
onSuccess: () => {
queryClient.invalidateQueries("posts");
}
Updating the cache manually (https://react-query.tanstack.com/guides/invalidations-from-mutations)
// Update post example
// I get the updated post data for onSuccess
onSuccess: (data) => {
queryClient.setQueryData("posts", (oldData) => {
const index = oldData.findIndex((post) => post.id === data.id);
if (index > -1) {
return [
...oldData.slice(0, index),
data,
...oldData.slice(index + 1),
];
}
});
},
I understand that manual update has the advantage of not doing an extra call for fetching the 'posts', but I wonder if there is any advantage of invalidating cache over the manual update. For example:
import { useMutation, useQueryClient } from "react-query";
const { API_URL } = process.env;
const createPost = async (payload) => {
const options = {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
};
if (API_URL) {
try {
const response = await fetch(API_URL, options);
if (!response.ok) {
throw new Error(response.statusText);
}
return response.json();
} catch (error) {
throw new Error(error);
}
} else {
throw new Error("No api url is set");
}
};
export default function useCreatePost() {
const queryClient = useQueryClient();
return useMutation((payload) => createPost(payload), {
// DOES INVALIDATING HAVE ANY ADVANTAGE OVER MANUAL UPDATE IN THIS CASE?
// onSuccess: () => {
// queryClient.invalidateQueries("posts");
// },
onSuccess: (data) => {
queryClient.setQueryData("posts", (oldData) => {
return [...oldData, data];
});
},
});
}
Thanks for your time!

As you state it yourself, the only advantage is that you don't waste another network call to update data we already have.
Here we have a create and delete example.
import { useMutation, useQueryClient } from 'react-query'
const queryClient = useQueryClient()
// createPost(post: PostT) {
// const { data } = await http.post<{ post: PostT >('/posts', { post });
// return data.post;
// }
const mutation = useMutation(createPost, {
onSuccess: (post) => {
queryClient.setQueryData<PostT[]>(['posts'], (oldData || []) => [ ...oldData, post])
},
})
// deletePost(id: string) {
// await http.delete(`/posts/${id}`);
// }
const mutation = useMutation(deletePost, {
onSuccess: (_, id) => {
queryClient.setQueryData<PostT[]>(['posts'], (oldData || []) => oldData.filter((post) => id !== post.id)
},
})
Invalidating the query can also be an option is some cases. The query will be invalidated and the data will be marked as stale. This will trigger a refetching in the background. So you know for a fact that the data will be as fresh as possible.
This can be handy if you got:
multiple queries to update with data from a mutation
have a (difficult) nested data structure to update
import { useMutation, useQueryClient } from 'react-query'
const queryClient = useQueryClient()
const mutation = useMutation(createPost, {
onSuccess: () => {
queryClient.invalidateQueries('posts')
queryClient.invalidateQueries('meta')
queryClient.invalidateQueries('headers')
},
})
But it really is up to you.

The main advantage of using manual updates comes from the fact that you can do it before the request is sent to the server; so if you manually update after the request is successful, then there's not much of an advantage if the data that you get from the server doesn't need to be immediately present to the user & in those cases (which I have found to be the majority) you better off invalidating. when you use optimistic updates, you assume the request is successful before you send it to server & then if the request fails you just roll back your update. this way your action happens instantly which is a better UX than doing the action, showing a loading spinner or something & then showing the updated state. so I have found it more useful for giving instantaneous feedback to the user than saving an extra request to the server. in most cases (as in yours) you still need to invalidate the query after, because your manually added post doesn't have an id, so you should sync it with the list of posts from the server. so be very careful about that because if you reading from that id somewhere else in that page, it would be undefined & would throw an error. so at the end of the day your mutation is not a great candidate for optimistic update & you should be careful to handle all the problems that can come up with your posts value having a post with no id in it (as opposed to something like a follow action which is just changing a boolean value in your database & you can confidently mutate the cache & undo it if request was not successful). so if we assume that you can handle that problem your useMutation hook would be something like this:
return useMutation(
(payload) => {
queryClient.setQueryData("posts", (oldData) => {
return [...oldData, payload];
});
return createPost(payload);
},
{
onSettled: () => {
queryClient.invalidateQueries("posts");
},
}
);

Related

How to refetch/fetch after a series of mutation in graphql/apollo/react

Right now I have a use case to use two useMutations to create/update database. So the second one is depends on the success of the first one. And also the second mutation needs to be called in a loop, just think about that I have a array and I need loop through the array and apply the second mutation.
After all these mutation finished I have to refetch another api to update caches, because the cache would be impacted by the two mutations.
I am really struggling with how to achieve this.
From another post: Apollo Client - refetchQueries after multiple updates
I can do probably like
const [creatEnrollment] = useMutation(mut1)
const [updateEnrollment] = useMutation(mut2)
const [toFetch, {loading, error, data}] = useLazyQuery(UsersDocument)
await Promise.all([creatEnrollment(), updateEnrollment()])
const result = () => toFetch({
variables: {name: 'i'}
})
but the problem is 1. I need to execute second mutations after the first one; 2, I need to have an array that applied to second mutations one by one.
I also saw
here How can I wait for mutation execution in React Query?
we can use onSuccess
const mutate1 = useMutation((data) => axios.post('/something', { data }))
const mutate2 = useMutation(somethingResult) => axios.put('/somethingElse', { somethingResult })
<button onClick={() => {
mutate1.mutate('data', {
onSuccess: mutate2.mutate
})
}} />
But still 1. how to loop thru mutate2.mutate? and how to fetch after mutate2 finished
do like this????:
<button onClick={() => {
mutate1.mutate('data', {
onSuccess: mutate2.mutate
})
mutate2.mutate('data', {
onSuccess: query
})
}} />
Thank you for helping!!
You can have a function for useMutation and onSuccess the data which use get on success use other mutation
const mutationFuntion = (id) => {
// this is first mutation
return useMutation(
(newTitle) => axios
.patch(`/posts/${id}`, { title: newTitle })
.then(response => response.data),
{
// 💡 response of the mutation is passed to onSuccess
onSuccess: (data) => {
// call the api which will get all the latest update
},
}
)
}
Get the Data of first mutation
const [addTodo, { data, loading, error }] = mutationFuntion(//send data);
This is consecutive mutation I found it from this https://react-query-v3.tanstack.com/guides/mutations#consecutive-mutations doc
useMutation(addTodo, {
onSuccess: (data, error, variables, context) => {
// Will be called 3 times
},
})
['Todo 1', 'Todo 2', 'Todo 3'].forEach((todo) => {
mutate(todo, {
onSuccess: (data, error, variables, context) => {
// Will execute only once, for the last mutation (Todo 3),
// regardless which mutation resolves first
},
})
})
For handle the promise of every mutation call
const mutation = useMutation(addTodo)
try {
const todo = await mutation.mutateAsync(todo)
console.log(todo)
} catch (error) {
console.error(error)
} finally {
console.log('done')
}
Please you need to verify on what kind of object you want to call mutation in loop it array or some thing alse.

React-Query Prefetch query doesnt return value?

const getQueryService = () => {
return {
login: async (id): Promise<AuthLoginGoogleResponse> => {
try {
const result = await authApi.loginGooglePost({
idToken: {
id_token: id,
},
});
return result;
} catch (error) {
console.error("Google Login Fail", error);
}
},
};
};
// Mutation is only for updating and creating and deleting
const getMutationService = () => {
return {};
};
const useGoogleLogin = () => {
const queryClient = useQueryClient();
const queryService = getQueryService();
// const { data, isLoading } = useQuery('auth', queryService.login)
const mutationService = getMutationService();
const fetchLoginData = async (
tokenId
): Promise<AuthLoginGoogleResponse | void> => {
return await queryClient.prefetchQuery("auth", async() => {
return await queryService.login(tokenId);
});
};
return fetchLoginData;
};
I am sending token.Id to API using Post request and I am calling it from component however when I run debugger, preFetchquery is not returning the value retuned from result in getqueryservice function.
Is there a reason why preFetchQuery is not returning the return value from getQueryService.login?
because that's what prefetching does. According to the docs (emphasis mine):
prefetchQuery is an asynchronous method that can be used to prefetch a query before it is needed or rendered with useQuery and friends. The method works the same as fetchQuery except that it will not throw or return any data.
So prefetchQuery just puts data in the cache so that it can be picked up later by useQuery, hence the name: pre-fetch.
If you wan to get data returned, you can use queryClient.fetchQuery instead - but you'd also need to handle errors in case the fetch fails.
To be honest, I'm not sure why you are trying to achieve though. Judging from the code, it looks like you're trying to execute a query when the user wants to login. Please keep in mind that this is not what queries are for. Logging someone in is a prime example for a mutation.

Query never stops fetching after refetching or invalidating

Fetching for the first time works, same goes for resetting the query. The data is fresh right after, then becomes stale after 5 seconds.
However, I want to refetch or invalidate the queries after applying mutations, but whenever I do so, the data just keeps refetching and never returning:
DevTools scr showing data just refetching
Same when i use the DevTools directly.
My useQuery hook:
export const useFetchCandidates = (id: string) => {
return useQuery<Entry<IKandidatFields>[], Error>(
'candidates',
async () => {
const res = await getCandidates(id)
return res
},
{
staleTime: 5000,
cacheTime: 10,
}
)
}
Using the hook to access data:
const {
data: candidates,
}: UseQueryResult<Entry<IKandidatFields>[], Error> = useFetchCandidates(id!)
The mutation:
const mutation = useMutation(
() =>
sendCandidateToNextStage(candidateID, utlysning).then(() =>
getCandidates(id!)
),
{
onMutate: () => {
queryClient.cancelQueries(['candidates', id])
},
onSettled: () => {
queryClient.resetQueries(['candidates', id])
},
}
)
This was solved by using a get axios request instead of utilizing the cms client directly... still don't know why the one works instead of the other when both of them return the same object.
When using the contentful API client:
export async function getCandidates(
id: string
): Promise<Entry<IKandidatFields>[]> {
const res = await client
.getEntries<IKandidatFields>({
content_type: 'kandidat',
})
return res
}
it was constantly fetching and never worked.
However, when using an axios request instead
export const getCandidatesFromAPI = async (id: string) => {
const { data } = await axios.get(
`https://cdn.contentful.com/spaces/${spaceID}/environments/${environmentId}/entries?access_token=${accessToken}&content_type=${contentTypeKandidat}`
)
return data
}
as the mutation function, everything worked perfectly.

React Query optimistic update reverts cache before updating

I have a custom hook which I want to use to perform an optimistic update. I am encountering a weird issue where the optimistic update occurs, then the cache is immediately reverted back to the previous cache (not sure why), and then the onSuccess queries invalidate and set the cache to what my original optimistic update did.
What this looks like is my item is updated, reverted, and updated again (blinking). For some reason, this only works with a few of the mutations I do. I think specifically larger mutations.
Can you tell any reason why this revert is happening? Thanks!
export const useFormMutation = (formId: string) => {
const queryClient = useQueryClient();
return useMutation(
(newForm: IForm) => {
return fetch(
"MY URL",
{
method: "POST",
body: JSON.stringify(newForm),
}
).then((response) => response.json());
},
{
onMutate: async (newForm) => {
await queryClient.cancelQueries(["form", formId]);
const previousForm = queryClient.getQueryData(["form", formId]);
queryClient.setQueryData(["form", formId], () => newForm);
return previousForm;
},
onSuccess: () => {
queryClient.invalidateQueries(["form", formId]);
},
}
);
};

How can I avoid "auto-update" cache when using `react-apollo-hooks` and `useSubscription` hook

I have some Apollo-Hooks code that uses useSubscription to listen for event changes in a subscription:
useSubscription<MySubscriptionUpdated>(MySubscription, {
onSubscriptionData: async ({ client, subscriptionData: { data } }) => {
if (!data) {
return;
}
...
This code automatically updates the cache on the response, which is great in most circumstances
However, I need to do some result-processing after the response is received, yet prior to the cache being updated.
Does anyone know of a way to use useSubscription hook, and not have the cache be automatically updated?
The response will ultimately always have an entity with __typename in it.
You can change fetchPolicy for each subscription. The default value is cache-first. To disable cache must set fetchPolicy to no-cache. For get more detail see apollo official document.
useSubscription<MySubscriptionUpdated>(MySubscription, {
fetchPolicy: "no-cache",
onSubscriptionData: async ({ client, subscriptionData: { data } }) => {
if (!data) {
return;
}
...
So, you can do a manual cache update, it would look something like this
apollo.mutate({
mutation: createTaskMutation,
variables: item,
update: (cache, { data }) => {
try {
let { allTasks } = cache.readQuery({ query: getTasks });
allTasks.push(data);
cache.writeQuery({ //
query: getTasks,
data: {
'allTasks': allTasks
}
});
} catch (e) {
// We should always catch here,
// as the cache may be empty or the query may fail
}
});

Resources