T
TanStack2y ago
fair-rose

setQueryData changes in v5?

I'm upgrading my project from v4 to v5 of react query today and have just a couple typescript errors with some calls to setQueryData:
Argument of type 'Decimal' is not assignable to parameter of type 'Updater<void | undefined, void | undefined>'
Argument of type 'Decimal' is not assignable to parameter of type 'Updater<void | undefined, void | undefined>'
and
Argument of type 'IPriceInterval[]' is not assignable to parameter of type 'Updater<void | undefined, void | undefined>'
Argument of type 'IPriceInterval[]' is not assignable to parameter of type 'Updater<void | undefined, void | undefined>'
I have dozens of other calls to setQueryData that don't have any error here's an example where p is the Decimal type we're getting an error from:
export const useCurrentMarket = () => {
const queryClient = useQueryClient();
return useQuery({
queryKey: KEY_CURRENT_MARKET,
queryFn: async () => {
const current = await StockHoldingsApi.getCurrent();
current.prices.forEach((p, symbol) =>
queryClient.setQueryData([...KEY_CURRENT_MARKET, symbol], p)
);
return current;
},
staleTime: minutes(1),
});
};
export const useCurrentMarket = () => {
const queryClient = useQueryClient();
return useQuery({
queryKey: KEY_CURRENT_MARKET,
queryFn: async () => {
const current = await StockHoldingsApi.getCurrent();
current.prices.forEach((p, symbol) =>
queryClient.setQueryData([...KEY_CURRENT_MARKET, symbol], p)
);
return current;
},
staleTime: minutes(1),
});
};
Thanks in advance for any insight.
23 Replies
fair-rose
fair-roseOP2y ago
I should also mention this is only a typing issue, if I ts-ignore the lines it all works
mute-gold
mute-gold2y ago
Huh, is it only when you spread into query keys? For some reason this isn't matching the first overload of setQueryData
fair-rose
fair-roseOP2y ago
nope, I have examples spreading and it working so everywhere in my project that currently works both functionally and type-wise, the signature on setQueryData has updater: unknown, whereas the problem areas, pycharm reports the type as Updater<void | undefined, void | undefined>, not entirely sure why at the moment as an experiment, I tried a refactor and was able to appease the wonderful typing gods, though I'm not quite sure why
export const useCurrentMarket = () => {
const queryClient = useQueryClient();
return useQuery({
queryKey: KEY_CURRENT_MARKET,
queryFn: async () => {
const current = await StockHoldingsApi.getCurrent();
current.prices.forEach((p, symbol) =>
// queryClient.setQueryData([...KEY_CURRENT_MARKET, symbol], p)
setCurrentPrice(queryClient, symbol, p)
);
return current;
},
staleTime: minutes(1),
});
};

const setCurrentPrice = (qc: QueryClient, s: string, p: Decimal) => {
qc.setQueryData([...KEY_CURRENT_MARKET, s], p);
};
export const useCurrentMarket = () => {
const queryClient = useQueryClient();
return useQuery({
queryKey: KEY_CURRENT_MARKET,
queryFn: async () => {
const current = await StockHoldingsApi.getCurrent();
current.prices.forEach((p, symbol) =>
// queryClient.setQueryData([...KEY_CURRENT_MARKET, symbol], p)
setCurrentPrice(queryClient, symbol, p)
);
return current;
},
staleTime: minutes(1),
});
};

const setCurrentPrice = (qc: QueryClient, s: string, p: Decimal) => {
qc.setQueryData([...KEY_CURRENT_MARKET, s], p);
};
absent-sapphire
absent-sapphire2y ago
2 issues bother me 1. current.prices.forEach((p, symbol) => symbol is of type number, not a string 2. If KEY_CURRENT_MARKET is a read-only array it's probably inferred literally. When you spread it, it probably goes to smth more general like a string[] Of course, it's hard to tell what causes the issue without looking at the code and how everything is structured. Small details matter in TS
fair-rose
fair-roseOP2y ago
1. current.prices is actually a Map<string, Decimal> 2. KEY_CURRENT_MARKET is inferred, simply defined as const KEY_CURRENT_MARKET = ["currentValues"];
absent-sapphire
absent-sapphire2y ago
@SmilyContainer Show me please 1. StockHoldingsApi.getCurrent implementation 2. prices
fair-rose
fair-roseOP2y ago
public static async getCurrent(): Promise<ICurrentMarket> {
interface IAPIMarketHoliday {
name: string;
exchange: string;
status: string;
open?: string;
close?: string;
}

interface IAPICurrentMarket {
status: string;
prices: Record<string, string>;
holidays: IAPIMarketHoliday[];
}

let c: IAPICurrentMarket = await new GRequest("h/st/cu").fetch();

const parseTime = (t: string) => {
const today = toISODateString(new Date());
return utcToZonedTime(`${today}T${t}.000+00:00`, getBrowserTz());
};

return {
status: c.status,
prices: new Map(
Object.entries(c.prices).map(([s, p]) => [s, new Decimal(p)])
),
holidays: (c.holidays || []).map((h) => ({
...h,
open: h.open ? parseTime(h.open) : undefined,
close: h.close ? parseTime(h.close) : undefined,
})),
};
}
public static async getCurrent(): Promise<ICurrentMarket> {
interface IAPIMarketHoliday {
name: string;
exchange: string;
status: string;
open?: string;
close?: string;
}

interface IAPICurrentMarket {
status: string;
prices: Record<string, string>;
holidays: IAPIMarketHoliday[];
}

let c: IAPICurrentMarket = await new GRequest("h/st/cu").fetch();

const parseTime = (t: string) => {
const today = toISODateString(new Date());
return utcToZonedTime(`${today}T${t}.000+00:00`, getBrowserTz());
};

return {
status: c.status,
prices: new Map(
Object.entries(c.prices).map(([s, p]) => [s, new Decimal(p)])
),
holidays: (c.holidays || []).map((h) => ({
...h,
open: h.open ? parseTime(h.open) : undefined,
close: h.close ? parseTime(h.close) : undefined,
})),
};
}
interface definitions too
export interface IMarketHoliday {
name: string;
exchange: string;
status: string;
open?: Date;
close?: Date;
}

export interface ICurrentMarket {
status: string;
prices: Map<string, Decimal>;
holidays: IMarketHoliday[];
}
export interface IMarketHoliday {
name: string;
exchange: string;
status: string;
open?: Date;
close?: Date;
}

export interface ICurrentMarket {
status: string;
prices: Map<string, Decimal>;
holidays: IMarketHoliday[];
}
thanks for looking and let me know if anything is silly, it's a solo closed source project and I'm more of a backend/infra person so I don't get any feedback on my weird react/ts 😀
absent-sapphire
absent-sapphire2y ago
@SmilyContainer Did you update tanstack/query types when migrating to v5?
fair-rose
fair-roseOP2y ago
are they separate packages? I've always just had the one package:
 grep -i tanstack package.json
"@tanstack/react-query": "5.49.2",
"@tanstack/react-router": "1.28.1",
"@tanstack/router-devtools": "^1.28.4",
"@tanstack/router-vite-plugin": "^1.28.2",
 grep -i tanstack package.json
"@tanstack/react-query": "5.49.2",
"@tanstack/react-router": "1.28.1",
"@tanstack/router-devtools": "^1.28.4",
"@tanstack/router-vite-plugin": "^1.28.2",
absent-sapphire
absent-sapphire2y ago
I'm trying to break it with different approaches, but I have no succeeded yet 🙂
absent-sapphire
absent-sapphire2y ago
No description
absent-sapphire
absent-sapphire2y ago
F* yeah! I found it.
absent-sapphire
absent-sapphire2y ago
@SmilyContainer You need to add curly braces in your forEach fn and the error will go away.
No description
absent-sapphire
absent-sapphire2y ago
😀
fair-rose
fair-roseOP2y ago
wow! that's subtle, i'll confirm in a few after undoing my workaround
absent-sapphire
absent-sapphire2y ago
ForEach callback returns void so when you do not wrap it inside the curly braces you return a qc.setQueryData return type. Those 2 combine and break somewhere along the line. Unfortunately, that's the most info I can provide. Query has sophisticated types and it's hard to track down what happens under the hood.
fair-rose
fair-roseOP2y ago
yea, i took a look at Tk's PR that changed the types on this function in v5 and it's a complicated little PR from a typing perspective. I really appreciate the insight and especially the explanation
absent-sapphire
absent-sapphire2y ago
no problem 🙂
fair-rose
fair-roseOP2y ago
just sat back down, reverted the workaround, added the curly braces to the 3 instances of the error, all fixed
absent-sapphire
absent-sapphire2y ago
I'm glad we found the cause of the problem. I assume it works like this. 1. Ts knows that the return of the forEach callback is always void 2. Since you didn't provide any Types to setQueryData and you returned it from the callback, TS assigns void to return type of setQueryData 3. Then it backfires going back from the return type all the way up the setQueryData types trying to infer what types of arguments can satisfy the return type void 4. That's how you got this error
fair-rose
fair-roseOP2y ago
really interesting, should i open up an issue on github for this or is this a lesson for me that "you shouldn't let forEach unnecessarily return something"?
absent-sapphire
absent-sapphire2y ago
It's not necessary problem with types, it's a normal ts behavior when it tries to infer types based on the info it has. It's just not obvious because error makes you think that something wrong with the argument, when in reality problem lies in the return type. It's better just keep in mind that you should not return anything from the foreach cb
fair-rose
fair-roseOP2y ago
great context for me, I really appreciate the insights, i'll have to remember that and maybe just use for loops more often to avoid the issue

Did you find this page helpful?