T
TanStack2y ago
metropolitan-bronze

Is a query not supposed to be generated upon page refresh?

I've observed this behavior where navigation from Page 1 to Page 2 adds the desired queries to the query cache, such that mutations that invalidate the target queries results in the desired refetches. However, if I refresh Page 2, which calls custom hooks that in turn call useQuery, and then call the mutation method, the queries never appear in the cache to be invalidated, and therefore no refetch happens. Is this expected behavior? Is there a way to resolve the difference in behavior between navigation and page refresh? Thank you in advance. The custom hook that implements the useQuery call: hooks/useGetIndividuals.tsx
...
import { QueryFunctionContext, useQuery } from "@tanstack/react-query";

export default function useGetIndividuals(collectionUrl: string) {
const [errorMsg, setErrorMsg] = useState<string>("");
const queryKey: [string, string] = ["individualsFor", collectionUrl];
const { isLoading, isError, data } = useQuery({
queryKey: queryKey,
queryFn: async (context: QueryFunctionContext<[string, string]>) => {
const [, collectionUrl] = context.queryKey;
try {
const response = await axios.get(
"/api/collection/" + collectionUrl + "/individuals"
);
return response?.data?.individuals || [];
} catch (e: any) {
...
setErrorMsg(e.message);
}
},
});

return { isLoading, isError, data, errorMsg };
}
...
import { QueryFunctionContext, useQuery } from "@tanstack/react-query";

export default function useGetIndividuals(collectionUrl: string) {
const [errorMsg, setErrorMsg] = useState<string>("");
const queryKey: [string, string] = ["individualsFor", collectionUrl];
const { isLoading, isError, data } = useQuery({
queryKey: queryKey,
queryFn: async (context: QueryFunctionContext<[string, string]>) => {
const [, collectionUrl] = context.queryKey;
try {
const response = await axios.get(
"/api/collection/" + collectionUrl + "/individuals"
);
return response?.data?.individuals || [];
} catch (e: any) {
...
setErrorMsg(e.message);
}
},
});

return { isLoading, isError, data, errorMsg };
}
The component that employs said hook: pages/collection/[urlPath]/index.tsx
...
const CollectionView: React.FC = () => {
const queryClient = useQueryClient();
...
const {
isLoading: isLoadingIndividuals,
isError: isErrorIndividuals,
data: individualsData,
errorMsg: errorMsgIndividuals,
} = useGetIndividuals(localUrlPathAsString);
...
};
export default CollectionView;
...
const CollectionView: React.FC = () => {
const queryClient = useQueryClient();
...
const {
isLoading: isLoadingIndividuals,
isError: isErrorIndividuals,
data: individualsData,
errorMsg: errorMsgIndividuals,
} = useGetIndividuals(localUrlPathAsString);
...
};
export default CollectionView;
21 Replies
deep-jade
deep-jade2y ago
What are you doing with that useState? 😅
metropolitan-bronze
metropolitan-bronzeOP2y ago
The errorMsg one? I’m certainly open to better solutions. Do you think that’s somehow part of the problem?
deep-jade
deep-jade2y ago
Regarding your issue: have you looked into query devtools? If that does not help, show a code sandbox
metropolitan-bronze
metropolitan-bronzeOP2y ago
have you looked into query devtools
I have. I can clearly see that there are no queries in the query cache once the page gets reloaded. Which is weird because the page calls useQuery() ... indirectly via a custom hook. I'm wondering whether this means that there's something I don't understand about query + component lifecycles. I've been working on a reproducible example, but can I expect code sandbox to behave the same with respect to page reloads as a normal implementation of a react/next app?
blank-aquamarine
blank-aquamarine2y ago
yes, while the sandbox is opened in an iframe, you can expand it to open in a new tab "refreshing" a page is the same as opening a page in a new tab or in a new browser. This shouldn't behave any different than opening the page for the first time
metropolitan-bronze
metropolitan-bronzeOP2y ago
Ok... I've managed to make an example that's similar enough. I'm hoping that it's in the same family of problem as I'm experiencing with my current app. If you go here, https://stackblitz.com/edit/github-uv9nor-jr6sa4?file=hooks%2FuseUpdateCollection.tsx, Click on the "view" icon in the collections page to visit the NewHotness single-collection page, Click "Add new video to collection", type whatever you want in the text input field that appears, and click, "Add". You should see in the developer console that it's being very inconsistent when something is in the query cache and when it isn't as well as when it's invalidating. I'm only seeing the invalidation happen successfully sometimes. Any leads on this? Perhaps that will help solve my problem, and if not, it will certainly help me advance the reproducible example. Thanks again in advance for any help!
StackBlitz
Query invalidation minimal example (forked) - StackBlitz
Run official live example code for Next.js Api Routes, created by Vercel on StackBlitz
deep-jade
deep-jade2y ago
'minimal' 😄 maybe update "react-query": "^3.39.3" to a recent version and this might be inconsistent because you are not awaiting promises also: no need to interact with the cache directly... queryClient.getQueryData should be enough
metropolitan-bronze
metropolitan-bronzeOP2y ago
Thank you. I will act on those asap and follow up. Much appreciated.
metropolitan-bronze
metropolitan-bronzeOP2y ago
Ok... I believe that I have updated the example code as advised above: https://stackblitz.com/edit/github-uv9nor-jr6sa4?file=pages%2Fcollection%2F%5BurlPath%5D.tsx I still can't quite figure out what is wrong. The first time I add a new video, it looks like the queryClient.invalidateQuery() doesn't succeed (by evidence of the identical queryData before and after the invalidation). The second time I add a new video, there is no longer any queryData at all... and in neither case is the UI updated. Now I'm just really confused.
Mark
StackBlitz
Query invalidation minimal example (forked) - StackBlitz
Run official live example code for Next.js Api Routes, created by Vercel on StackBlitz
deep-jade
deep-jade2y ago
I’ll try to look into this later. A more minimal example would really help
metropolitan-bronze
metropolitan-bronzeOP2y ago
Thank you. I’ll see what I can pare down in the meantime. But I wanted to two separate pages in order to try to reproduce the behavior I’m seeing on my actual project. While we’re not quite seeing the same behavior in this example, I feel like resolving the issues herein will help me figure out what I’m doing wrong more generally.
metropolitan-bronze
metropolitan-bronzeOP2y ago
Mark
StackBlitz
Query invalidation minimal example 5 jan - StackBlitz
Run official live example code for Next.js Api Routes, created by Vercel on StackBlitz
metropolitan-bronze
metropolitan-bronzeOP2y ago
(it's just the two hooks and the one pages/collection/[urlPath].tsx that should matter.)
eastern-cyan
eastern-cyan2y ago
I think I know where the problem might be. In _app.tsx, you have the following line.
const queryClient = new QueryClient();
const queryClient = new QueryClient();
Nextjs re-renders the _app component during navigation in order to inject the new page component in. When you refresh the page in development, react re-renders everything twice in order to make strict mode work properly (this is in development only). The second re-render in development is only for checks that react performs and is not used for UI updates. With this, every time the _app component is re-rendered, we are creating a new QueryClient, throwing away any previous cached data. In development, the second time that react rerenders _app is only for checks. So, you have a situation where, the latest new query client isn't connected to the UI stuff. So, the queries used on the initial page refresh are all thrown away without reference. On subsequent navigation, only queries that are used for that page are cached (We create a new query client on every page navigation). SOLUTION ----- wrap queryClient in a memo like below so we use the same instance between re-renders
const queryClient = useMemo(() => new QueryClient(), []);
const queryClient = useMemo(() => new QueryClient(), []);
Make sure you remove all the other new QueryClient() everywhere except _app.tsx and just use useQuery() hook. Let me know if this helps / doesn't help.
deep-jade
deep-jade2y ago
Or even better: useState and using the eslint plugin
metropolitan-bronze
metropolitan-bronzeOP2y ago
Can you clarify what you mean? Thank you so much for this suggestion! That makes sense. I’ll try and report back.
deep-jade
deep-jade2y ago
Stable Query Client | TanStack Query Docs
The QueryClient contains the QueryCache, so you'd only want to create one instance of the QueryClient for the lifecycle of your application - not a new instance on every render. Exception: It's allowed to create a new QueryClient inside an async Server Component, because the async function is only called once on the server.
metropolitan-bronze
metropolitan-bronzeOP2y ago
I see. Thank you!
eastern-cyan
eastern-cyan2y ago
This is good stuff. Thanks for pointing out 👍 What's the relation with eslint? One advantage for using useMemo I find is that you can automatically regenerate the queryClient if it depends on some attributes like API token or some other item.
ratty-blush
ratty-blush2y ago
Instead of creating a new queryClient, how about you just clear/reset your current one if your API token changes. As for the eslint relationship, they're just plugin rules that are recommended to use to catch common mistakes.
metropolitan-bronze
metropolitan-bronzeOP2y ago
This totally solved my problem. Thank you so much, everyone!!!

Did you find this page helpful?