T
TanStack•3y ago
fascinating-indigo

Invalidating dependent queries?

I've just started tinkering with Tanstack Query in React this week, and it's been an absolute joy to use. I'm building a simple weather app, and I want to give the user the opportunity to refetch data without refreshing the page. Obviously, I can use queryClient.invalidateQueries() for that, but I can't wrap my head around dealing with the dependent query I have. I am getting the user's current location with the Geolocation API in a useGeolocationQuery, which is a variable in the dependent useForecastQuery. When the user presses the refresh button, it should first retry the geolocation query. If this has changed, it will obviously fetch the new forecast. However, if it has not changed, it should still get the latest forecast. The easy solution would be to refetch both queries immediately, but I'm dealing with usage limits so I would really like to avoid fetching the latest forecast for an old location (because the refetches would be parallel). So, how can I best implement this? Apologies if this is answered/documented somewhere already!
6 Replies
foreign-sapphire
foreign-sapphire•3y ago
Hmmm 🤔 Not sure if this works, but you could try setting the forecastQuery to disabled (enabled: false) when the geolocationQuery is in isFetching state...
fascinating-indigo
fascinating-indigoOP•3y ago
Unfortunately, it doesn't seem to work in the minimal example I've pasted below. The "yesterday" query gets ran twice. I guess that is to be expected, as both would be invalidated in parallel, so isFetching would still be false.
import { useQuery, useQueryClient } from "@tanstack/react-query";

function useTimeQuery() {
return useQuery({
queryKey: ["time"],
queryFn: async () => {
console.log("running time query");
await new Promise((resolve) => setTimeout(resolve, 1000));
return new Date();
},
staleTime: 60 * 1000,
});
}

function useYesterdayQuery(
date: Date | undefined,
opts?: { enabled?: boolean }
) {
return useQuery({
queryKey: ["yesterday", date],
queryFn: async () => {
console.log("running yesterday query", date?.getTime());
await new Promise((resolve) => setTimeout(resolve, 1000));
return new Date(date!.getTime() - 24 * 60 * 60 * 1000);
},
staleTime: 60 * 1000,
gcTime: 0,
...opts,
});
}

export default function Test() {
const time = useTimeQuery();
const yesterday = useYesterdayQuery(time.data, {
enabled: !!time.data && !time.isFetching,
});

const queryClient = useQueryClient();

return (
<div>
<div>Today: {time.data?.toLocaleString()}</div>
<div>Yesterday: {yesterday.data?.toLocaleString()}</div>
<button
onClick={() => {
console.log("invalidating")
queryClient.invalidateQueries({});
}}
>
Invalidate
</button>
</div>
);
}
import { useQuery, useQueryClient } from "@tanstack/react-query";

function useTimeQuery() {
return useQuery({
queryKey: ["time"],
queryFn: async () => {
console.log("running time query");
await new Promise((resolve) => setTimeout(resolve, 1000));
return new Date();
},
staleTime: 60 * 1000,
});
}

function useYesterdayQuery(
date: Date | undefined,
opts?: { enabled?: boolean }
) {
return useQuery({
queryKey: ["yesterday", date],
queryFn: async () => {
console.log("running yesterday query", date?.getTime());
await new Promise((resolve) => setTimeout(resolve, 1000));
return new Date(date!.getTime() - 24 * 60 * 60 * 1000);
},
staleTime: 60 * 1000,
gcTime: 0,
...opts,
});
}

export default function Test() {
const time = useTimeQuery();
const yesterday = useYesterdayQuery(time.data, {
enabled: !!time.data && !time.isFetching,
});

const queryClient = useQueryClient();

return (
<div>
<div>Today: {time.data?.toLocaleString()}</div>
<div>Yesterday: {yesterday.data?.toLocaleString()}</div>
<button
onClick={() => {
console.log("invalidating")
queryClient.invalidateQueries({});
}}
>
Invalidate
</button>
</div>
);
}
No description
foreign-sapphire
foreign-sapphire•3y ago
yeah it was just a shot in the dark 😅
fascinating-indigo
fascinating-indigoOP•3y ago
Haha, I was still hopeful!
flat-fuchsia
flat-fuchsia•3y ago
Can you combine both queries into one and fetch both the location and the forecast for that location inside the queryFn? Like:
const loc = await getLocation();

const forecast = await getForecast(loc);

return {loc, forecast};
const loc = await getLocation();

const forecast = await getForecast(loc);

return {loc, forecast};
fascinating-indigo
fascinating-indigoOP•3y ago
I could, but as I'm allowing users to select locations other than their current one, I think I would have to duplicate each currently dependent query (which in reality is more than one forecast query from different APIs with different staleTimes) for one which grabs the current geolocation and one which doesn't, which would be far from ideal. But that would be the way to go if I don't find a more elegant way, yeah! That wouldn't actually be true, I could of course just do...
async (input: Coord | null) => {
const location = input ?? await getLocation();
const forecast = await getForecast(location);
return { location, forecast };
};
async (input: Coord | null) => {
const location = input ?? await getLocation();
const forecast = await getForecast(location);
return { location, forecast };
};
That still loses me some of the niceties of having the location in a query, but it would actually be workable. Figured it out! The trick was finding out that unlike .invalidateQueries(), .refetch() returns the updated QueryObserverResult, which I can use to figure out whether the geolocation has changed, in which case I need to manually invalidate all forecast queries. Thanks to you both!
function useRefetch({ refetchGeolocation }: { refetchGeolocation: boolean }) {
const queryClient = useQueryClient();
const geolocation = useGeolocationQuery({ enabled: refetchGeolocation });

async function refetch() {
const latest = refetchGeolocation ? await geolocation.refetch() : null;
if (!latest || compareCoordinates(geolocation.data, latest.data))
await queryClient.invalidateQueries({ queryKey: ["forecast"] });
}

return refetch;
}
function useRefetch({ refetchGeolocation }: { refetchGeolocation: boolean }) {
const queryClient = useQueryClient();
const geolocation = useGeolocationQuery({ enabled: refetchGeolocation });

async function refetch() {
const latest = refetchGeolocation ? await geolocation.refetch() : null;
if (!latest || compareCoordinates(geolocation.data, latest.data))
await queryClient.invalidateQueries({ queryKey: ["forecast"] });
}

return refetch;
}

Did you find this page helpful?