T
TanStack14mo ago
deep-jade

Dynamic dependent queries

Hi, I'm not sure what's the way to go about this situation
const useOwnedTokensMetadata = (chain) => {
const {data: accounts} = useQuery({
querykey: ['accounts', chain],
queryFn: () => fetchAccounts(chain),
})
const {data: ownedTokens} = useQuery({
queryKey: ['ownedTokens', accounts, chain],
queryFn: accounts ? () => fetchOwnedTokens(accounts, chain) : skipToken,
})
const {data: ownedNfts} = useQuery({
queryKey: ['ownedNfts', accounts, chain],
queryFn: accounts && shouldFetchNfts(chain) ? () => fetchOwnedNfts(accounts, chain) : skipToken,
})
const {data: ownedTokensMetadata} = useQuery({
queryKey: ['tokensMetadata', ownedTokens, ownedNfts, chain],
queryFn: ownedTokens && (ownedNfts || !shouldFetchNfts(chain))
? () => fetchMetadataFor(ownedTokens, ownedNfts, chain),
: skipToken,
})
return {data: ownedTokensMetadata}
}
const useAllOwnedTokensMetadata = (chains) => {
return useQueries({
queries: chains.map(
// What to do? Can't map them onto hooks.
(chain) => useOwnedTokensMetadata(chain)
),
combine: useCallback(...),
})
}
const useOwnedTokensMetadata = (chain) => {
const {data: accounts} = useQuery({
querykey: ['accounts', chain],
queryFn: () => fetchAccounts(chain),
})
const {data: ownedTokens} = useQuery({
queryKey: ['ownedTokens', accounts, chain],
queryFn: accounts ? () => fetchOwnedTokens(accounts, chain) : skipToken,
})
const {data: ownedNfts} = useQuery({
queryKey: ['ownedNfts', accounts, chain],
queryFn: accounts && shouldFetchNfts(chain) ? () => fetchOwnedNfts(accounts, chain) : skipToken,
})
const {data: ownedTokensMetadata} = useQuery({
queryKey: ['tokensMetadata', ownedTokens, ownedNfts, chain],
queryFn: ownedTokens && (ownedNfts || !shouldFetchNfts(chain))
? () => fetchMetadataFor(ownedTokens, ownedNfts, chain),
: skipToken,
})
return {data: ownedTokensMetadata}
}
const useAllOwnedTokensMetadata = (chains) => {
return useQueries({
queries: chains.map(
// What to do? Can't map them onto hooks.
(chain) => useOwnedTokensMetadata(chain)
),
combine: useCallback(...),
})
}
My idea was something like this
const composedTokensMetadataOptions = (chain) => queryOptions({
queryKey: ['composedTokensMetadata', chain],
queryFn: async () => {
const accounts = await queryClient.fetchQuery(accountsOptions(chain))
const ownedTokens = await queryClient.fetchQuery(ownedTokensOptions(accounts, chain))
const ownedNfts = shouldFetchNfts(chain) ? await queryClient.fetchQuery(ownedNftsOptions(accounts, chain)) : []
return await fetchMetadataFor(ownedTokens, ownedNfts, chain)
}
})
const useAllOwnedTokensMetadata = (chains) => {
return useQueries({
queries: chains.map(
(chain) => composedTokensMetadataOptions(chain),
),
combine: useCallback(...),
})
}
const composedTokensMetadataOptions = (chain) => queryOptions({
queryKey: ['composedTokensMetadata', chain],
queryFn: async () => {
const accounts = await queryClient.fetchQuery(accountsOptions(chain))
const ownedTokens = await queryClient.fetchQuery(ownedTokensOptions(accounts, chain))
const ownedNfts = shouldFetchNfts(chain) ? await queryClient.fetchQuery(ownedNftsOptions(accounts, chain)) : []
return await fetchMetadataFor(ownedTokens, ownedNfts, chain)
}
})
const useAllOwnedTokensMetadata = (chains) => {
return useQueries({
queries: chains.map(
(chain) => composedTokensMetadataOptions(chain),
),
combine: useCallback(...),
})
}
But I've heard it isn't recommended to call fetchQuery inside queryFn. So what's the right way to do this? Thanks!
13 Replies
magic-amber
magic-amber14mo ago
Why don't you do it like this?
const composedTokensMetadataOptions = (chain) => queryOptions({
queryKey: ['composedTokensMetadata', chain],
queryFn: async () => {
const accounts = await fetchAccounts(chain);
const ownedTokens = await fetchOwnedTokens(accounts, chain);
const ownedNfts = shouldFetchNfts(chain) ? await fetchOwnedNfts(accounts, chain) : [];
return await fetchMetadataFor(ownedTokens, ownedNfts, chain);
}
})
const composedTokensMetadataOptions = (chain) => queryOptions({
queryKey: ['composedTokensMetadata', chain],
queryFn: async () => {
const accounts = await fetchAccounts(chain);
const ownedTokens = await fetchOwnedTokens(accounts, chain);
const ownedNfts = shouldFetchNfts(chain) ? await fetchOwnedNfts(accounts, chain) : [];
return await fetchMetadataFor(ownedTokens, ownedNfts, chain);
}
})
I'm not sure about ownedNfts syntax, but everything else is just a simple chain of fetches, you don't need fetchQuery for it.
deep-jade
deep-jadeOP14mo ago
@denis.monastyrskyi But then if I mount a component with
useQuery({
queryKey: [‘accounts’, chain],
queryFn: () => fetchAccounts(chain),
refetchInterval: 60_000,
})
useQuery({
queryKey: [‘accounts’, chain],
queryFn: () => fetchAccounts(chain),
refetchInterval: 60_000,
})
then it will fall out of sync with useAllOwnedTokensMetadata, wont it?
magic-amber
magic-amber14mo ago
It's 2 different queries. 1. ['accounts', chain] 2.['composedTokensMetadata', chain] They store different values. If you provide the same chain to both of them they will have the same accounts because you used the same endpoint for accounts in both of them, but they will make 2 separate request to get accounts
deep-jade
deep-jadeOP14mo ago
but suppose the user adds an account and i refetch [‘accounts’, chain]. Then there will be one more account returned by that query than [‘composedTokensMetadata’, chain] computes over, so there will be missing metadata for tokens owned by that new account
magic-amber
magic-amber14mo ago
When you create a new account, you will probably use useCreateAccountMutation. UseMutation has onSuccess hook where you should call queryClient.invalidateQueries and tell which queries should be refetched, In your case it will be
queryClient.invalidateQueries({ queryKey: ['accounts', chain] });
queryClient.invalidateQueries({ queryKey: ['composedTokensMetadata', chain] });
queryClient.invalidateQueries({ queryKey: ['accounts', chain] });
queryClient.invalidateQueries({ queryKey: ['composedTokensMetadata', chain] });
deep-jade
deep-jadeOP14mo ago
unfortunately the server is a third party service, so the react app only reads from it. Accounts are added by a completely separate mobile app So the react app doesnt fire any mutations This is why i need to refetch in intervals to detect changes in the server state
magic-amber
magic-amber14mo ago
Then you must use this ['composedTokensMetadata', chain] only. It already incapsulates accounts, and you must use refetchInterval. You see, you can not rely on that your accounts will be in sync with other data, because even if you refetch accounts every 60 seconds where are guarantees that other data are in sync with accounts? Thus you must refetch all that composedTokensMetadata
deep-jade
deep-jadeOP14mo ago
The server is guaranteed to be the source of truth. In the original example with useOwnedTokensMetadata, it all worked flawlessly, because the dependencies were encoded in the queryKeys. The problem is when I need to use useQueries. It is not feasible to rely only on composedTokensMetadata, because it takes super long to fetch and some components only need the list of accounts.
magic-amber
magic-amber14mo ago
useOwnedTokensMetadata - is a good solution. But if you want to refetch it every 60secs it is not different from what I suggessted. It's the same approach but on a different level. useAllOwnedTokensMetadata - will be way worth, because if single call to useOwnedTokensMetadata creates a waterfall of 4 requests, then you can imagine what will happen if you call chains.map. Here's what docs say: Dependent queries by definition constitutes a form of request waterfall, which hurts performance. If we pretend both queries take the same amount of time, doing them serially instead of in parallel always takes twice as much time
deep-jade
deep-jadeOP14mo ago
Yes the docs say that dependent queries by definition cause waterfalls, so they can't be avoided in this situation.
useOwnedTokensMetadata - is a good solution. But if you want to refetch it every 60secs it is not different from what I suggessted.
I believe it is different, because the query key is effectively
['tokensMetadata',
[
...account1OwnedTokens,
...account2OwnedTokens,
...//etc
],
chain,
]
['tokensMetadata',
[
...account1OwnedTokens,
...account2OwnedTokens,
...//etc
],
chain,
]
So if a refetch of ['accounts', query] introduces accounts with more tokens, then the queryKey for tokensMetadata will change and the queryFn will run. It's basically what the eslint rule encourages to keep everything in sync
magic-amber
magic-amber14mo ago
['tokensMetadata', ownedTokens, ownedNfts, chain] - TokensMetadata depends on [ownedTokens, ownedNfts, chain] right or not?
deep-jade
deep-jadeOP14mo ago
Yes, but ownedTokens depends on accounts So transitively tokensMetadata also depends on accounts
magic-amber
magic-amber14mo ago
[ownedTokens, ownedNfts] - depends on the account. So my solution was the same as your
const useComposedTokensMetadata = (chain) => {
return useQuery({
// Store everything under composedTokensMetadata key and refetch every 60 secs
queryKey: ['composedTokensMetadata', chain],
queryFn: async () => {
// Await for accounts
const accounts = await fetchAccounts(chain);
// Run both in parallel
const promise1 = fetchOwnedTokens(accounts, chain);
const promise2 = shouldFetchNfts(chain) ? fetchOwnedNfts(accounts, chain) : Promise.resolve([]);
// Await results
const [ownedTokens, ownedNfts] = await Promise.all([promise1, promise2])
// Fetch metadata
const metadata = await fetchMetadataFor(ownedTokens, ownedNfts, chain);
return {accounts, owedTokens, ownedNfts, metadata};
},
refetchInterval: 60_000,
staleTime: Infinite, // you don't refetch on component render, you refetch every 60 secs
});
}

// Usage
function App() {
const {data: { accounts } } = useComposedTokensMetadata();
// OR
const {data: { metadata } } = useComposedTokensMetadata();
return null;
}
const useComposedTokensMetadata = (chain) => {
return useQuery({
// Store everything under composedTokensMetadata key and refetch every 60 secs
queryKey: ['composedTokensMetadata', chain],
queryFn: async () => {
// Await for accounts
const accounts = await fetchAccounts(chain);
// Run both in parallel
const promise1 = fetchOwnedTokens(accounts, chain);
const promise2 = shouldFetchNfts(chain) ? fetchOwnedNfts(accounts, chain) : Promise.resolve([]);
// Await results
const [ownedTokens, ownedNfts] = await Promise.all([promise1, promise2])
// Fetch metadata
const metadata = await fetchMetadataFor(ownedTokens, ownedNfts, chain);
return {accounts, owedTokens, ownedNfts, metadata};
},
refetchInterval: 60_000,
staleTime: Infinite, // you don't refetch on component render, you refetch every 60 secs
});
}

// Usage
function App() {
const {data: { accounts } } = useComposedTokensMetadata();
// OR
const {data: { metadata } } = useComposedTokensMetadata();
return null;
}
Now you can continue to improve this and do this
async function fetchEverything(chain) {
// Await for accounts
const accounts = await fetchAccounts(chain);
// Run both in parallel
const promise1 = fetchOwnedTokens(accounts, chain);
const promise2 = shouldFetchNfts(chain) ? fetchOwnedNfts(accounts, chain) : Promise.resolve([]);
// Await results
const [ownedTokens, ownedNfts] = await Promise.all([promise1, promise2])
// Fetch metadata
const metadata = await fetchMetadataFor(ownedTokens, ownedNfts, chain);
return {accounts, owedTokens, ownedNfts, metadata};
}

const useComposedTokensMetadata = (chain) => {
return useQuery({
// Store everything under composedTokensMetadata key and refetch every 60 secs
queryKey: ['composedTokensMetadata', chain],
queryFn: () => fetchEverything(chain),
refetchInterval: 60_000,
staleTime: Infinite, // you don't refetch on component render, you refetch every 60 secs
});
}

const useAllOwnedTokensMetadata = (chains) => {
return useQueries({
queries: chains.map(
(chain) => fetchEverything(chain),
),
combine: useCallback(...),
})
}
async function fetchEverything(chain) {
// Await for accounts
const accounts = await fetchAccounts(chain);
// Run both in parallel
const promise1 = fetchOwnedTokens(accounts, chain);
const promise2 = shouldFetchNfts(chain) ? fetchOwnedNfts(accounts, chain) : Promise.resolve([]);
// Await results
const [ownedTokens, ownedNfts] = await Promise.all([promise1, promise2])
// Fetch metadata
const metadata = await fetchMetadataFor(ownedTokens, ownedNfts, chain);
return {accounts, owedTokens, ownedNfts, metadata};
}

const useComposedTokensMetadata = (chain) => {
return useQuery({
// Store everything under composedTokensMetadata key and refetch every 60 secs
queryKey: ['composedTokensMetadata', chain],
queryFn: () => fetchEverything(chain),
refetchInterval: 60_000,
staleTime: Infinite, // you don't refetch on component render, you refetch every 60 secs
});
}

const useAllOwnedTokensMetadata = (chains) => {
return useQueries({
queries: chains.map(
(chain) => fetchEverything(chain),
),
combine: useCallback(...),
})
}

Did you find this page helpful?