Synchronize UI updates of multiple useQueries #2646
Replies: 3 comments 3 replies
-
That sounds like an interesting problem, I'll have to think a bit about that one. |
Beta Was this translation helpful? Give feedback.
-
So, I think for initial loading, the idea is quite easily to check the loading states of all queries, possibly with For background updates, like after query invalidation, it's a bit more tricky. First of all, it wouldn't be as "jarring" because your 5 queries already have data, so there would only be a layout shift of all of them would return different data. If that is "expected" or likely to happen, you can just fallback to showing a big loading spinner instead (see above). if you really want to basically "keep previous data" until all 5 queries have finished fetching, and then update the ui once with all the new data, I think you would have to roll it manually, something like:
that could be abstracted away in a custom hook, but I think it's a niche use-case |
Beta Was this translation helpful? Give feedback.
-
Here's another use-case where this batching could be useful: const Component = () => {
const { data: todoItem } = useQuery(/* ... */);
// The component used to display our entity depends on its status
if (todoItem.status === "todo") {
return <EditTodoForm item={todoItem} />;
}
/* ... */
};
const useSaveTodo = () => {
return useMutation({
/* ... */
onSuccess: async () => {
// Invalidate both the list query and the details query
await Promise.all([
queryClient.invalidateQueries({ queryKey: "todos" }),
queryClient.invalidateQueries({ queryKey: `todo/${todoItem.id}` }),
]);
},
});
const EditTodoForm = ({ todoItem }) => {
const { mutate, isPending } = useSaveTodo();
// The requirements here are: save the change, show a loader in the button while ensuring all data is up-to-date, and the go back to the "list view" which should already be up-to-date
const onConfirm = () => {
mutate(/* ... */, {
onSuccess: navigation.goBack
});
}
return (
<>
{/* ... */}
<Button onPress={mutate} isLoading={isPending}></Button>
</>
);
}; When running the mutation, if it results in
Here's the userland implementation we're currently using to "batch-commit invalidation results"import {
type FetchQueryOptions,
type QueryKey,
skipToken,
} from "@tanstack/react-query";
import { queryClient } from "#shared/query-client/queryClient";
/**
* Invalidate multiple queries and wait for all of them to be fetched before saving to the query cache and triggering React re-renders
*/
export const batchInvalidate = async <TQueryKey extends QueryKey>(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
queryOptions: FetchQueryOptions<any, Error, any, TQueryKey>[],
) => {
for (const options of queryOptions) {
// Mark queries as invalidated, but don't refetch them yet
await queryClient.invalidateQueries({
queryKey: options.queryKey,
refetchType: "none",
});
}
const queriesToRefetch = queryOptions
.map((options) => {
// Select all relevant active queries, including the ones specified with partial keys
return queryClient.getQueryCache().findAll({
queryKey: options.queryKey,
type: "active",
});
})
.flat();
let pendingCount = queriesToRefetch.length;
let markAllInvalidationsDone: () => void;
const allInvalidationsDonePromise = new Promise<void>((resolve) => {
markAllInvalidationsDone = resolve;
});
const markOneReady = () => {
pendingCount -= 1;
if (pendingCount === 0) {
markAllInvalidationsDone();
}
};
await Promise.allSettled(
queriesToRefetch.map((query) => {
const { queryKey, queryFn, ...options } = query.options;
if (!Array.isArray(queryKey) || !queryFn || queryFn === skipToken) {
markOneReady();
return Promise.resolve();
}
return queryClient.fetchQuery({
...options,
// eslint-disable-next-line @tanstack/query/exhaustive-deps
queryKey,
queryFn: async (context) => {
let result;
try {
result = await queryFn(context);
} finally {
markOneReady();
}
// Wait for all invalidations in the batch to be done
await allInvalidationsDonePromise;
// Commit to query cache, this will trigger React re-renders
return result;
},
});
}),
);
}; |
Beta Was this translation helpful? Give feedback.
-
Is it possible to synchronize the UI updates of multiple useQuery calls (with different keys)?
Let's say I have 5 different useQuery calls in different parts of the UI. If the fetches all take between 100 - 300 ms to complete, I assume I'll end up with the UI updating 5 times within 200 ms - a somewhat jarring user experience.
The same can be said when using invalidateQueries after a mutation - if I invalidate multiple queries at once, the UI will update a lot in a short amount of time.
Is there a way to tell React Query to batch UI updates that happen in quick succession? I guess the useQuery part could be solved with Suspense, but with invalidateQueries I don't want revalidating queries to suspend - I want them to show the old data until all of them are refetched (within a given threshold perhaps).
Beta Was this translation helpful? Give feedback.
All reactions