T
TanStack3y ago
harsh-harlequin

How to refresh credentials that are used within queryFn without making them part of the cacheKey?

I have a scenario where credentials need to be refreshed every x minutes for subsequent requests to succeed.
Currently the queryFn uses old cached credentials despite the react state containing the updated credentials. I assume queryFn is using some memorisation in some fashion internally. How do I ensure refetches use the latest credentials instead of the initial ones that have now expired? I don't want to add the credentials to the cacheKey as it will unnecessarily invalidate existing long lived cache data.
6 Replies
absent-sapphire
absent-sapphire3y ago
Who wrote the queryFn? I’ve never had an issue with this and I typically use tokens that expire every few minutes…
ambitious-aqua
ambitious-aqua3y ago
Pretty likely a stale closure problem. Please show some code. One way would be to put the token in a ref instead
harsh-harlequin
harsh-harlequinOP3y ago
Its a fairly complicated example, so difficult to setup a sand box, but here it is: I have the following hook:
export const useAggregations = (
integrations: Record<string, Integration> | undefined,
sources: Source[] = [],
periods: Period[]
) => {
console.log('integrations', integrations); // Here I always see the latest versions

const queries = useMemo(
() =>
sources.reduce<UseQueryOptions<AggregationResult, Error>[]>((queriesAgg, source) => {
console.log('reduce', integrations); // Here I always see the latest versions

const integration = integrations?.[source.integration];
periods.forEach( // PERIODS ARE just TIME RANGES OF DATA TO GET FROM THE SERVER, Where cacheType is not 'short' we don't expect the data to change on the server,
({ start, end, period, cacheType }) => {
const cacheKey = CACHE_KEY_AGGREGATION({
integrationId: integration?.id, // integrationId remains constant when new integration credentials arrive in props.
cacheType,
period,
start,
end
}); // All of these are part of the cacheKey in a strutured way
queriesAgg.push({
keepPreviousData: true,
queryKey: cacheKey,
refetchOnMount: cacheType === 'short' ? 'always' : false,
queryFn: async () => {

console.log('queryFn batch', integrations); // HERE I always see the initial integrations from the first request.

// I LOAD A BATCH QUERY HERE PASSING THROUGH THE integration TO USE.
// The integration contains the temp credentials i need to use but the creds are not PART OF THE QUERY KEY.
return batchAggregationsLoader
.load({
integration: integrations?.[source.integration] as Integration,
..................
})
},
onSuccess: () => {
console.debug('Updated cache for', ...cacheKey);
},
enabled: Boolean(integration) && Boolean(source) && Boolean(start) && Boolean(end)
});
}
);

return queriesAgg;
}, []),
[batchAggregationsLoader, integrations, sources, periods]
);

return useQueries({ queries });
}
export const useAggregations = (
integrations: Record<string, Integration> | undefined,
sources: Source[] = [],
periods: Period[]
) => {
console.log('integrations', integrations); // Here I always see the latest versions

const queries = useMemo(
() =>
sources.reduce<UseQueryOptions<AggregationResult, Error>[]>((queriesAgg, source) => {
console.log('reduce', integrations); // Here I always see the latest versions

const integration = integrations?.[source.integration];
periods.forEach( // PERIODS ARE just TIME RANGES OF DATA TO GET FROM THE SERVER, Where cacheType is not 'short' we don't expect the data to change on the server,
({ start, end, period, cacheType }) => {
const cacheKey = CACHE_KEY_AGGREGATION({
integrationId: integration?.id, // integrationId remains constant when new integration credentials arrive in props.
cacheType,
period,
start,
end
}); // All of these are part of the cacheKey in a strutured way
queriesAgg.push({
keepPreviousData: true,
queryKey: cacheKey,
refetchOnMount: cacheType === 'short' ? 'always' : false,
queryFn: async () => {

console.log('queryFn batch', integrations); // HERE I always see the initial integrations from the first request.

// I LOAD A BATCH QUERY HERE PASSING THROUGH THE integration TO USE.
// The integration contains the temp credentials i need to use but the creds are not PART OF THE QUERY KEY.
return batchAggregationsLoader
.load({
integration: integrations?.[source.integration] as Integration,
..................
})
},
onSuccess: () => {
console.debug('Updated cache for', ...cacheKey);
},
enabled: Boolean(integration) && Boolean(source) && Boolean(start) && Boolean(end)
});
}
);

return queriesAgg;
}, []),
[batchAggregationsLoader, integrations, sources, periods]
);

return useQueries({ queries });
}
If I add the integration credentials (or a hash of them) to the cache key then it fixes the problem. But this is obviously undesirable as I then unnecessarily refetches all the queries.
sunny-green
sunny-green3y ago
Does integration receive a new reference or is it just mutated? If just mutated it wouldn't trigger useMemo to refresh. Does it work if you remove useMemo (for testing)?
ambitious-aqua
ambitious-aqua3y ago
useMemo is not the problem. it also doesn't do anything meaningful, so it could be safely removed. if integrations is react state, then it should be part of the queryKey. if it's just a token, then the question is: why is it part of the react lifecycle? is your component re-rendering if the credentials change? as I said, you can put them in a useRef and update them, then access them with .current in the queryFn to bypass it. But the better solution would be to take things that shouldn't re-render your component or re-run your queryFn out of the react lifecycle altogether
harsh-harlequin
harsh-harlequinOP3y ago
Ok thanks, I think this makes sense. The integration contains other data which does need to cause a render. hence why it was inside. I'll look into separating the two, to keep them seperate Thanks for your responses!

Did you find this page helpful?