I have a database collection that can be updated by a large number of users in real-time. Users need to be able to see updates from other members, but the collection is large, so re-downloading the entire collection every time is massively inefficient/expensive.
Every document has a updateTime
timestamp. Given this, what I want to do is poll for updated data as needed (either on component mount or at other frequencies) and merge that into the data already stored in cache with ReactQuery (using persistClientQuery
).
I'm new to ReactQuery, so I'm wondering if there's a more efficient approach to than the one I'm using here, where I use React Query's useInfiniteQuery
hook with the newest updateTime
as the nextPageParam
:
The query itself:
export function useTalentWithStore() {
const queryClient = useQueryClient();
const query = useInfiniteQuery<Talent[]>({
queryKey: ['talent'],
getNextPageParam: (lastPage, allPages) => {
const data = allPages.flat();
// Use the newest document's updateTime as the next page param
const times = data.map((tal) => tal?.docMeta?.updateTime);
if (data.length) {
const max = Math.max(...times as any);
return new Date(max);
}
return undefined;
},
queryFn: async ({ pageParam }) => {
let talentQuery: firebase.firestore.CollectionReference<firebase.firestore.DocumentData> | firebase.firestore.Query<firebase.firestore.DocumentData>
= firestore.collection("talent");
// If the there's a page param, just get documents updated since then, otherwise, get everything
if (pageParam) {
talentQuery = talentQuery.where("docMeta.updateTime", ">", pageParam);
}
let talentSnapshot = await talentQuery.get();
const talentUpdates: Talent[] = talentSnapshot.docs.map((doc) => {
return {
id: doc.id,
...doc.data()
}
});
return talentUpdates;
},
staleTime: Infinity,
});
// Combine new data with any old data, and return a flat object
const flatData = useMemo<Talent[] | undefined>(() => {
const oldData = query.data?.pages?.[0] || [];
const newData = query.data?.pages?.[1] || [];
const combinedData: Talent[] = [];
if (oldData) {
combinedData.push(...oldData);
}
for (const tal of newData) {
const idx: number = combinedData.findIndex((t) => t.id === tal.id);
if (idx >= 0) {
combinedData[idx] = tal;
} else {
combinedData.push(tal);
}
}
// If there's any old data, flush it out and replace it with the combined new data
if (oldData.length) {
queryClient.setQueryData(['talent'], (data: any) => ({
pages: [combinedData],
pageParams: query.data?.pageParams,
}));
}
return combinedData;
}, [query.data, queryClient]);
return { ...query, flatData };
}
Example Usage:
const talentQuery = useTalentWithStore();
const talent = talentQuery.flatData;
const [fetchedOnMount, setFetchedOnMount] = useState(false);
useEffect(() => {
if (!fetchedOnMount && !talentQuery.isFetching) {
console.log(`Fetching New Talent`, !fetchedOnMount && !talentQuery.isFetching);
talentQuery.fetchNextPage();
}
setFetchedOnMount(true);
}, [talentQuery, fetchedOnMount]);
Is all this really necessary, or does ReactQuery support this functionality natively?
If not, are there other approaches I should consider here or pitfalls I need to watch out for?
(Note: While this code uses Firestore, for various reasons, I don't want to use Firestore's real-time updates here)
Answering this for anybody else who stumbles along!
There's a much simpler solution, which is basically to use a normal
query
and passqueryClient
into thequeryFn
so you can use it to grab any old data and filter the query with the date/time of the most recent record.If you set the staleTime to
Infinity
(as done previously) and setrefetchOnMount
toalways
, you'll get a query that saves the previous results, fetches any updates whenever the query remounts, and merges in those updates using whatever merge function you specify.