I have a very basic app that fetches a user and allows to change his name. I fetch the user with React query so I can benefit from the cache feature. It works.
However, when I want to update the user, I use a classic post request with axios. Once the user is updated in the database, I need to update the cache directly in the updateUser() function. I've seen tutorials on the web that use queryCache.setCache, but it doesn't work here. How to fix this? Or maybe there is a better way to handle such queries?
Also, I notice a huge number of rendering... (see the "user render" console.log in the app file).
For the convenience, I've made a small script on a codesandbox with the pokeapi:
https://codesandbox.io/s/api-service-syxl8?file=/src/App.js:295-306
Any help would be greatly appreciated!
So, I'll show you what I do:
const updateUser = async (userUpdates: User) => {
const data = await UserService.updateUser(userUpdates); // return axios data
return data;
}
// if you want optimistic updating:
const { mutate: mutateUser } = useMutation(updateUser, {
onMutate: async (userUpdates) => {
// Cancel any outgoing refetches (so they don't overwrite our optimistic update)
await queryClient.cancelQueries(['user', userUpdates.id]);
// Snapshot the previous value
const previousUser = queryClient.getQueryData(['user', userUpdates.id]);
// Optimistically update to the new value
queryClient.setQueryData(['user', userUpdates.id], userUpdates);
// Return a context with the previous user and updated user
return { previousUser, userUpdates }; // context
},
// If the mutation fails, use the context we returned above
onError: (err, userUpdates, context) => {
queryClient.setQueryData(['user', context.userUpdates.id], context.previousUser);
},
// Always refetch after error or success:
onSettled: (userUpdates) => {
queryClient.invalidateQueries(['user', userUpdates.id]);
}
});
// then to update the user
const handleUpdateUser = (userUpdates: User) => mutateUser(userUpdates);
This all comes from the docs:
Optimistic Updates
Related
I have two functions that run asynchronously getting data from the API. Both of them are called from their own useEffect().
I have a third function that needs to run once those two functions have been fully completed.
How can this be accomplished?
Edit:
Both of the async functions look like this:
useEffect(() => {
fetchBudgetBucketsData();
}, [fiscalYear]);
useEffect(() => {
fetchBudgetBucketsData();
}, [fiscalYear]);
const fetchBudgetsData = async () => {
setIsFetchingBudgets(true);
const res = await getBudgets(orgID, `${parseInt(fiscalYear)}`, '', budgetType);
setIsFetchingBudgets(false);
if (isErrorResponse(res)) {
console.warn(res.details);
message.error(res.displayText);
return;
}
setBudgets(res.budgets);
};
const fetchBudgetBucketsData = async () => {
setIsLoadingBudgetBuckets(true);
if (orgID === undefined) {
return;
}
const res = await getBudgetBuckets(orgID, fiscalYear);
setIsLoadingBudgetBuckets(false);
if (isErrorResponse(res)) {
console.warn(res.details);
message.error(res.displayText);
return;
}
setBudgetBuckets(res.buckets);
};
Whenever the budget data or bucket data is updated, I want to call another function that checks for errors. However when the page loads, I need it to wait for both of those functions to be finished before it checks for errors.
Edit #2:
After some debugging, it looks like the issue might have to do with when React updates the state. Since I am trying to check for errors in data saved in the state.
One way could be chaining Promises.
Promise.all([ApiCall1, ApiCall2])
// At this point two promises above will be resolved
.then(() => ApiCall3)
Read more
I discovered the issue was caused by how React chooses when to update the state and not how I was calling these functions asynchronously.
I was able to call my Error check function by hooking it into the output of the data fetch calls. This makes sure that the error check only runs when either the budgets or buckets are edited and finished being changed.
useEffect(() => {
getWarningsAndErrors();
}, [budgets, budgetBuckets]) //Update errors whenever we edit budgets or buckets
I use the react-query library to get my data.
When the user changes, I would love it if the previous user data was removed automatically & new data was fetched.
This does, not happen though, the api gets called a couple times more with the old userId, and only after 1 or 2 times re-focussing on the app, it will fetch the data with the proper new userId.
Here is the hook:
When I log some info in the getData function, I can see it being called a couple times with the old userId after logging out.
export default function useData() {
const {user} = useAuth();
const queryClient = useQueryClient();
useEffect(() => {
queryClient.removeQueries('data')
queryClient.invalidateQueries()
}, [user]);
return useQuery('data', () => getData(user!.uid), {
enabled: !!user,
})
}
Does anyone know how I can remove all data when my user changes?
All dependencies of your query should be in the query key, because react-query automatically refetches when the key changes. This is also in the docs here.
In your case, this means adding the user id:
useQuery(
['data', user?.uid],
() => getData(user!.uid),
{
enabled: !!user,
}
)
For clearing up old cache entries, I would possibly suggest setting cacheTime: 0 on your query, which means they will be garbage collected as soon as the last observer unmounts. Calling queryClient.removeQueries manually is also an option.
I've configured react-query with infinite stale time, per docs, like this:
<ReactQueryConfigProvider config={{
queries: {
staleTime: Infinity
}
}}>
Most of my queries appropriately never go stale, except one, my 'profile' query:
const getProfile = async () => {
if (!isAuthenticated()) {
return null;
}
try {
const response = await axios.get('/user/profile');
return response.data;
}
catch (error) {
errorCheck(error);
}
};
export const useProfile = () =>
useQuery('profile', getProfile);
This is the query that holds the current user's profile. isAuthenticated() is a synchronous call that checks to see if we have a user token (so I don't make API calls that I know will fail).
For some reason, in the react-query devtools window, this query shows as stale immediately. I really don't see what I'm doing differently with this one. Any suggestions for debugging this?
Here's what I think the issue was, and how I solved it.
Because I set staleTime: Infinity in my ReactQueryConfigProider, I expected all of my queries to never go stale.
What's different about this query is I invalidate it when something not driven by the UI happens.
I have a session timer in my code that, when the session expired, calls queryCache.invalidateQueries('profile') to trigger any UI displaying the profile to re-render.
It appears that if invalidateQueries is ever called outside the context of a query, the settings in ReactQueryConfigProider are not observed, so staleTime is set to the default, 0.
To resolve this, for the queries I need to invalidate on a timer, I added { staletime: Infinity } to the query explicitly:
export const useProfile = () => {
const { data: session } = useSession();
const userId = session?.userId;
return useQuery(['profile', userId], getProfile, { staleTime: Infinity });
};
I won't go so far as to say this is a bug in react-query, but this seems to be a workaround.
I ran into the same problem, and it was caused by a component that had a child component which used react-query. Check your component tree and make sure nothing uses useProfile() outside of <ReactQueryConfigProvider>.
Let's say I fetch some data. Then, I make some modifications to the data - for example, I click 'delete' on a single record, and an appropriate request is made to update the remote database.
Now, what's the most popular way to keep local state (the view) synchronized? Should I just delete the record from the local state, and hope the DB indeed got updated and everything's in sync? Or perhaps I should instantly make yet another request and fetch the entirety of the updated data and populate the view?
What i suggest is that you assign the original state to a variable and then you make the call to the database and update the state by deleting the entry and then you check the response from the db if it is ok so you keep the new state, if not you re-set the state with the original that you stored on the variable and show an error message, this is a basic example using axios
import React, {useState, useEffect} from 'react';
import axios from 'axios';
import { notification } from 'antd';
export default const myCommponent = () => {
const [items, setItems] = useState([])
const [fetchingData, setFetchingData] = useState(true)
// make the call to the db to get the items
useEffect(() => {
// here you create the function that will fetch the data from the api once the component is mounted
async function geData() {
await axios({
url: 'your_url',
method: 'GET'
}).then((response) => {
setItems(response.data)
setFetchingData(false)
});
});
}
if (fetchData) {
// if the fetchingData is true so you send the request this to prevent your component from changing the state multiple times
geData();
}
}, []);
const handleDelete = (e) => {
// here you imutate the state
const originalList = [ ...items ];
// you delete the item from the imutated state
const newList = originalList.filter((item) => {
return item.key !== e.key;
});
// you set the new state
setItems(newList);
// you send your request to the api to delete the desired item
axios({
url: `delete url/`,
method: 'DELETE'
}).then((response) => {
// you check the response sent from the api
if (response.data.status_code === 200) {
// if it OK so you just have to show a confirmation message
notification['success']({
message: response.data.message
});
} else {
// if not yuou show an error message
notification['error']({
message: response.data.message
});
// you reset the state to the original with the deleted item.
setItems(originalList);
}
});
};
render(){
return(
<div>
// return the component here
</div>
);
}
}
I hope that will give you the desired result.
There are a few ways this can be done.
Sync
Make the network call to delete the item
Wait for its reponse
Show a loading/wait state meanwhile
Once you have received the response, update the view accordingly i.e. if it's an error, tell the user that
Async
Make the network call to delete the item
Update the view as soon as the network call is made but save the initial values
Once you receive the response if there is an error, you need to add back that item to your view
If it's a success, you need to do nothing
The advantage of 2 is that you provide the feedback to the user immediately and it gives the impression that your app is fast no matter how slow their internet is. You are handling everything in the background. I believe it can be bad for sensitive things that you NEED to be updated and the user may not see or get back to those places once they have done an action there.
So, doing stuff like simply archiving a conversation can be handled by 2nd method.
Doing stuff like blocking a user should be handled by 1st method because it can be sensitive to the user. If you do handle it by 2nd you need to convey it clearly to the user that the action has failed if it fails.
I am working on project with Apollo on client side. I am using react-apollo-hooks on my client side. And I have a problem with useApolloClient.
When i fire query with my client I got in useApolloClient I don't get back all data I need. FetchMore is missing. If I use regular query (useQuery) I get that. But problem is I need to fire that query on click and i need to use one provided with apollo client.
I have this function for fetching data on click:
const bulkSearch = async data => {
setContent(<Spinner />);
showModal();
try {
const response = await client.query({
query: BULK_SEARCH_PRODUCTS,
variables: { data }
});
if (!response.loading) {
setContent(
<ProductsListDisplay
products={response.data.bulkSearch.products}
fetchMore={response.fetchMore}
count={{ total: 10 }}
/>
);
return 200;
}
} catch (err) {
return 400;
}
};
And response doesn't contain fetchMore.
On the other way classic query returns fetchMore.
const newdata = useQuery(BULK_SEARCH_PRODUCTS, {
variables: { data: { ids: ["536003", "513010"] } }
});
Some help ? Thank you!
According to the apollo-client docs, ApolloClient.query returns a Promise that resolves to an ApolloQueryResult, which is a simpler object that has only data, errors, loading, networkStatus, and stale as properties.
On the other hand, the render prop argument of react-apollo's Query component gets fed a much richer object, with fetchMore being one of its additional properties. If you want to do something similar using the raw ApolloClient object, you would have to use ApolloClient.watchQuery, which returns an ObservableQuery that you can subscribe to consume results. The benefit of this is that you have access to more methods, such as ObservableQuery.fetchMore.
Note this approach is fundamentally different than using ApolloClient.query, since that function requests one query and returns the result as a Promise, while ApolloClient.watchQuery consistently monitors your cache and pushes updated results to your subscribe method when the cache store changes, so it's a more complicated lifecycle. In general, if you're already using react-apollo or one of the #apollo/react-X packages, you probably want to stay away from ApolloClient.watchQuery, since the functionality from those libraries builds directly on top of it and is designed to be easier to consume.
Hope this helps!
You have to create your own FetchMore method for this. This has to be handled by you that's the safer you to go.
In my case I needed
fetchMore
Adding Infinite loading and should event deal with loading state as well.
Problem with default loading state is that it will be always false as return of promise.
When you use await client.query.
In our query we have cursor based pagination.
read this
Create Function that will trigger on scroll ( end of page )
Check on value of after and update it with state management
Loading as well as data also needs to be in state.
Code:
const fetchMoreFilteredData = async (after) => {
try {
setFilteredLoading(true); // set this state in order to show loading indicator
const { data, loading } = await client.query({
query: QUERY,
variables: {
after: after,
...all variables,
},
fetchPolicy: "network-only",
notifyOnNetworkStatusChange: true,
});
const {
query: {
pageInfo: { hasNextPage, endCursor },
},
} = data;
setFilteredData({
// update your data ...filteredData,
});
setHasNextPage(hasNextPage); // check if there is next page
setEndCursor(endCursor); // set end cursor for next page this will guide the query to fetch next page
setFilteredLoading(loading); // set loading state to false
} catch (error) {
error.graphQLErrors.map((error) => {
console.log("error", error.message);
});
setFilteredLoading(false);
} };
const handleLoadMore = () => {
hasNextPage && fetchMoreFilteredData(_endCursor);
};