A real-time logistics grid that renders 30,000 live rows.
A legacy Windows-Forms desktop tool ran a logistics operation: hundreds of vehicles, thousands of records, constant change. We rebuilt it as a cloud React app without losing the density operators relied on. Thousands of records in one high-performance grid, and the live position of every vehicle on a map, updating in real time.
A desktop-grade tool, rebuilt for the browser
The client is a German logistics-technology provider whose software helps companies cut time, distance, cost and carbon across the logistics chain. Their operational tool was a legacy Windows-Forms desktop application: fast and dense, but stuck on the desktop.
The brief was to bring it to the cloud as a React web app without giving up what made the desktop version useful: the ability to render thousands of records from hundreds of vehicles in one high-performance grid, and to show their real-time locations and routes on an interactive map.
Four hard requirements
Each one is reasonable alone. Together, in a browser, in real time, they force real engineering.
A 30,000-row data grid
Filtering, sorting, grouping, aggregation, pagination, editing, virtualization and resizing, across multiple tables.
Real-time updates
Reflect every change a driver makes on their device in the grid as it happens.
Interactive live maps
Plot the live locations and chosen routes of hundreds of vehicles on a map.
High performance
Minimize queries to the database and calls to the servers. Keep it fast under load.
30,000 rows can't all live in the DOM
Mount 30,000 rows and the browser falls over. Mount only what's visible and your filtering, sorting and grouping go wrong. We needed both.
The answer was to separate what’s rendered from what exists. We virtualized the grid with React’s virtual DOM so only the rows in the viewport (roughly 120 at a time) are ever mounted. But we kept the full dataset in the browser, so filtering, sorting, grouping and aggregation operate across all 30,000 records, not just the visible slice. Scrolling stays smooth; the numbers stay correct.
1// 30,000 rows must stay consistent for filtering, sorting and grouping, so we2// keep the FULL dataset in the browser (IndexedDB) and only mount the rows that3// are actually visible in the viewport.4const { data: all } = useQuery({5 queryKey: ['fleet'],6 queryFn: () => db.vehicles.toArray(), // full set from IndexedDB, not the API7 staleTime: Infinity,8});9 10const rows = useMemo(11 () => group(sort(filter(all ?? [], filters), sortBy), groupBy),12 [all, filters, sortBy, groupBy], // derived across ALL rows, not a page13);14 15const virtual = useVirtualizer({16 count: rows.length, // could be 30,00017 estimateSize: () => 32,18 overscan: 12,19 getScrollElement: () => scrollRef.current,20});21 22// only virtual.getVirtualItems() (~120 rows) ever reach the DOM- Virtualization & resizing: only viewport rows mounted; columns resize freely.
- Filtering, sorting, grouping, aggregation, computed across the entire dataset.
- Editing & pagination: inline edits and paging that stay consistent with the source.
Where do 30,000 rows actually live?
Holding the full dataset in the browser solved consistency, and immediately raised the question of how to store that much, fast.
We cached with React Query and persisted the data in the browser’s IndexedDB. The result was lightning-fast interaction with almost no chatter to the backend. The grid reads from a local store, not the network, so filtering and scrolling never wait on a round-trip.
| Naive fetch-per-view | IndexedDB + React Query | |
|---|---|---|
| Backend requests | On every scroll / filter | Once, then served locally |
| Filter / sort / group | Round-trip to the server | Instant, across all 30,000 rows |
| Consistency | Viewport only | Full dataset in the browser |
Hundreds of events a minute, one source of truth
Drivers change state constantly. Every one of those changes had to land in every grid and the map, instantly and consistently.
We drove real-time updates with SignalR. Rather than refetch, each incoming event patches IndexedDB directly and then nudges React Query, so every grid and the map re-derive from the same local source. That design handles hundreds of events a minute and reflects them instantly across multiple grids holding vast amounts of data.
1// Hundreds of driver events a minute. Each one patches IndexedDB directly, then2// nudges React Query, so every grid AND the map re-derive from one source.3connection.on('VehicleMoved', async (evt: VehicleEvent) => {4 await db.vehicles.update(evt.id, {5 lat: evt.lat, lng: evt.lng, status: evt.status, eta: evt.eta,6 });7 8 // refresh derived state without a network refetch, the data is already local9 queryClient.invalidateQueries({ queryKey: ['fleet'], refetchType: 'none' });10 mapBus.emit('vehicle:moved', evt); // move the live marker on the map11});12 13await connection.start(); // SignalR, with auto-reconnectHundreds of vehicles, moving live
The grid tells you what's happening; the map shows you where.
We modified react-leaflet to plot the live locations and chosen routes of hundreds of vehicles, driven by the very same event stream and IndexedDB store that feeds the grids. Markers move as the events arrive, so the map and the grids are always telling the same story: a super-fast, real-time, highly interactive application.
Teaching the old system to speak to the new one
Modernization rarely happens in one clean cut. The old and new worlds have to coexist.
To get events out of the legacy world, we built a custom project that consumes the legacy system’s events and feeds them into the modern web app’s real-time pipeline. The old Windows-Forms system and the new React app stay in sync during and after the transition, letting the client migrate with confidence rather than in a single risky leap.
What it runs on
React
The cloud web app that replaced the legacy Windows-Forms desktop tool.
React grid (customized)
A customized data grid for virtualized rendering of tens of thousands of rows.
React Query
Caching that keeps the app fast and the backend quiet.
IndexedDB
The full dataset, persisted in the browser as the local source of truth.
SignalR
Real-time events from driver devices, handled at hundreds per minute.
react-leaflet (modified)
Interactive maps with live vehicle positions and routes.
C# / .NET
The backend services and the bridge that consumes legacy events.
Identity Server
Authentication and authorization across the application.
Desktop density, browser reach, real-time everything
Records per grid, windowed to ~120 mounted rows
Real-time vehicle events a minute, reflected across every grid
Source of truth (IndexedDB) feeding every grid and the map
- A super-fast, real-time, interactive web app replacing a legacy desktop tool.
- Thousands of records rendered smoothly with virtualization and local caching.
- Live maps and grids in lockstep, driven by one event stream.
- A guided deployment with full quality assurance, in partnership with the client.
Put the data where the work happens
The unlock wasn’t a faster grid component. It was moving the data to where the work happens. With the full dataset cached locally in IndexedDB, virtualization, real-time updates and live maps all became local operations: fast, consistent, and quiet on the network. A legacy desktop tool became a modern, real-time web app without losing an ounce of its density.
Got a data-dense, real-time app that needs to feel instant in the browser? We’ve done it at scale.
Talk to engineering