Parallel fetch & abort Had a challenge to fetch thousands of items in batches Every batch is 5k lines If we reach the end, then next returned batch from api is an empty array Loading to be done in parallel to save time How to solved launch 20 fetch functions at the same time, which is more than enough for my situation associate every fetch with abort controller track order of fetches and abort controllers as soon as fetch returns an empty array all next fetches are aborted
const controllers: AbortController[] = []
const maxQueries = 20
const itemsPerQuery = 5000
for (let i = 0; i <= maxQueries; i++) {
controllers.push(new AbortController())
}
const fetchSmallBatch = async (): Promise<Receipt[]> => {
const res = await fetch(url.receipts({ start: '0', count: '50' }), { credentials: 'include' })
if (!res.ok) throw Error('Problem fetching receipts')
const data: Receipt[] = await res.json()
return data
}
type Props = {
start: string
count: string
orderInQueue: number
}
const fetchBigBatch = async ({ start, count, orderInQueue }: Props): Promise<Receipt[]> => {
const controller = controllers[orderInQueue]
const signal = controller.signal
const res = await fetch(url.receipts({ start, count }), {
credentials: 'include',
signal
})
if (!res.ok) throw Error('Problem fetching receipts')
const data: Receipt[] = await res.json()
if (data.length === 0) {
for (let i = orderInQueue; i < controllers.length; i++) {
const isAborted = controllers[i].signal.aborted
if (!isAborted) {
controllers[i].abort('no data in prev portion --> no need to fetch this on')
}
}
}
return data
}
await fetchSmallBatch()
const promises = []
for (let i = 0; i <= maxQueries; i++) {
promises.push(fetchBigBatch({
start: String(i * itemsPerQuery),
count: String(itemsPerQuery),
orderInQueue: i
}))
}
const startTime = performance.now()
await Promise.allSettled(promises).then(data => {
console.log(data)
const endTime = performance.now()
console.log(`Fetching took ${endTime - startTime} ms`)
}).catch(error => {
console.log(error)
})
Parallel queries with useQueries https://tanstack.com/query/v4/docs/react/guides/parallel-queries We can make parallel queries with tanstack Nice thing is that queries in tanstack have a retry mechanism by default
import 'ag-grid-community/styles/ag-grid.css'
import 'ag-grid-community/styles/ag-theme-alpine.css'
import { AgGridReact } from 'ag-grid-react'
import { useEffect, useRef, useState } from 'react'
import { Box, LinearProgress } from '@mui/material'
import { columnDefs, defaultColDef } from './columnDefs'
import { layoutSlice } from 'shared/layouts'
import { AgGridCustomStyles } from './ui/AgGridCustomStyles'
import { dispatch } from 'shared/clients'
import type { JSX } from 'react'
import { type Receipt } from 'shared/types/Receipt'
import { useReceiptsQuery } from 'entities/receipts'
import { useQueries, type UseQueryResult } from '@tanstack/react-query'
import { url } from 'shared/url'
const queryFn = async ({ start, count }: { start: string, count: string }): Promise<Receipt[]> => {
const res = await fetch(url.receipts({ start, count }), { credentials: 'include' })
if (!res.ok) throw Error('Problem fetching receipts')
const data: Receipt[] = await res.json()
return data
}
const useLoad10kItems = (queryNumber: number): Array<UseQueryResult<Receipt[], unknown>> => {
const batchSize = 1000
const queries = useQueries({
queries: [...Array(10).keys()].map((number) => {
const start = String(10 * batchSize * (queryNumber - 1) + batchSize * number)
const count = String(batchSize)
return {
queryKey: ['receipts', { start, count }],
queryFn: async () => await queryFn({ start, count }),
staleTime: Infinity
}
})
})
return queries
}
export const ReceiptsTable = (): JSX.Element | null => {
const gridRef = useRef(null)
const { data: initReceipts, isLoading } = useReceiptsQuery({ start: '0', count: '10' })
const [attemptNum, setAttemptNum] = useState(1)
const queries = useLoad10kItems(attemptNum)
const [allReceipts, setAllReceipts] = useState<Receipt[]>([])
const areQueriesFetched = queries.every(query => query.isFetched)
const accumulatedReceipts = useRef<Receipt[]>([])
useEffect(() => {
if (!areQueriesFetched) return
const noMoreDataAvailable = queries.some(query => query.data !== undefined && query.data.length === 0)
const thereIsMoreDataAvailable = !noMoreDataAvailable
const receiptsFromQueries = queries.flatMap(query => query.data) as Receipt[]
if (noMoreDataAvailable) {
accumulatedReceipts.current.push(...receiptsFromQueries)
console.log(accumulatedReceipts.current.length)
setAllReceipts(accumulatedReceipts.current)
return
}
if (thereIsMoreDataAvailable) {
setAttemptNum(attemptNum + 1)
accumulatedReceipts.current.push(...receiptsFromQueries)
}
}, [areQueriesFetched])
return (
<Box
className='ag-theme-alpine ag-receipt-table'
sx={{ flexGrow: 1, position: 'relative', overflow: 'visible', height: '100%' }}
>
<AgGridCustomStyles />
{isLoading && <LinearProgress sx={{ height: '1px', top: '53px', zIndex: 2 }} />}
<AgGridReact<Receipt>
ref={gridRef}
rowData={allReceipts.length === 0 ? initReceipts : allReceipts}
animateRows
rowSelection='multiple'
suppressRowClickSelection
enableCellTextSelection
ensureDomOrder
suppressCellFocus
suppressContextMenu
columnDefs={columnDefs}
defaultColDef={defaultColDef}
suppressScrollOnNewData
onSelectionChanged={(params) => {
const selectedRows = params.api.getSelectedRows()
const isRowSelected = selectedRows.length > 0
if (isRowSelected) {
dispatch(layoutSlice.actions.showFooter())
return
}
dispatch(layoutSlice.actions.hideFooter())
}}
/>
</Box>
)
}