react-query: why is this one query always stale? - reactjs

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>.

Related

Why does adding an observer to react-query cause it to ignore its `enabled` property?

Update: I've now filed this as an issue over on react-query's Github, you should probably follow it there.
I have a pretty straightforward useQuery:
const refreshable = false // hard coded for the sake of example
const tokenQuery = useQuery('refresh-token', refreshQueryFn, {
enabled: refreshable,
refetchInterval: 1000 * 30,
refetchIntervalInBackground: true,
})
If this is stand-alone then no problems. Everything initialises, and unless refreshable is set true then it lies dormant.
However, in the same hook I added an observer...
useEffect(() => {
// Create an observer to watch the query and update its result into state
const observer = new QueryObserver(queryClient, {
queryKey: 'refresh-token',
})
const unsubscribe = observer.subscribe((queryResult) => {
console.log(
'Do something with the token!'
queryResult.data
)
})
// Clean up the subscription when the component unmounts
return () => {
unsubscribe()
}
}, [token, queryClient])
Attaching the observer causes a fetch (and failure, and multiple retries) to happen irrespective of enabled, which I don't think should happen.
Can anyone explain why this happens, before I raise an issue on react-query?
under the hood, useQuery creates a QueryObserver with the options you pass to it. So after the userQuery, you'll have one observer with enabled: false, which will give you a disabled query.
Now enabled is a per-observer property. It is totally legit to have one useQuery instance that is enabled while having another with the same queryKey that is disabled.
And this is basically what you are doing: When you manually create the new QueryObserver, you are not passing enabled: false to it, so you create a second observer that is enabled, which will then run the query.
So passing refreshable to the new Oberver's enabled option should solve it:
const observer = new QueryObserver(queryClient, {
queryKey: 'refresh-token',
enabled: refreshable,
})

React-query, query doesn't update arguments

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.

React-query: how to update the cache?

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

React Redux Firebase - isLoaded only false for the first document when using firestore

In my app I use react-redux-firebase version 2.5.1 with redux-firestore version ^0.13.0. I render a list of conversations with customers (called threads) and each time a user goes to another thread, until it's loaded I want him to see a loading spinner. The issue of mine is that only when I go to a thread for the first time after app deployment the spinner shows, after that each time I change a thread the method isLoaded is always true, even though I see that for some seconds the page shows the previous thread. Has anyone met with something like that and can help me how to solve it ?
Here is my component
const mapStateToProps = (state, props) => {
const customerId = props.match.params.id;
return {
thread: state.firestore.data.thread,
...
};
};
export default compose(
withStyles(styles),
connect(mapStateToProps, mapDispatchToProps),
firestoreConnect(props => [
{
collection: "thread",
doc: props.customerId,
storeAs: "thread"
}
...
])
)(ThreadView);
Here is my render method
render() {
const {firebase, thread} = this.props;
const res = !isLoaded(thread); // after the first load it's always false
console.log(`customerId: ${customerId}, isLoaded: ${res}`)
if (res) {
return <LoadingSpinner/>;
}
...
I also had this issue and so far haven't found a satisfactory solution. However, I did find a workaround:
Write a custom isLoaded function that checks for any requests in progress:
const checkIsLoaded = function(firestore) {
return !Object.values(firestore.status.requesting).find(isRequesting => isRequesting);
};
Because all the current pending request's statuses (true/false) are saved in the redux store under firestore.status.requesting we can check this, and see if any requests are currently still loading!
It's not an ideal solution because it will check ALL in-progress requests, not just the specific one you want to check. However, depending on your use case, this may be enough.

UseApolloClient query won't return fetchMore

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);
};

Resources