Skip to content
Zowork
All case studies
Engineering case study~12 min read

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.

ReactVirtualizationIndexedDBReact QuerySignalRLeaflet
fleet.grid30,000 rows
vehiclerouteetastatus
VH-0142DE-7 · Hub 309:14en route
VH-0188DE-2 · Hub 109:22en route
VH-0203NL-4 · Hub 509:31loading
VH-0231DE-9 · Hub 209:40en route
VH-0274FR-1 · Hub 409:48delayed
VH-0299DE-5 · Hub 309:57en route
VH-0312BE-3 · Hub 610:05idle
rendered 120 / 30,000
virtualized · IndexedDB-backed
Introduction

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.

The brief

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.

Root-cause deep-dive

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.

Virtualized over the full datasetuse-fleet-grid.ts
ts
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.
The next problem

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-viewIndexedDB + React Query
Backend requestsOn every scroll / filterOnce, then served locally
Filter / sort / groupRound-trip to the serverInstant, across all 30,000 rows
ConsistencyViewport onlyFull dataset in the browser
The payoff
Minimal data requests, maximal responsiveness. With the dataset already local, every interaction (and every real-time update) is a local operation.
Real-time

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.

Event → IndexedDB → everythingrealtime.ts
ts
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-reconnect
Driver event
device change
SignalR
~hundreds/min
IndexedDB
patched in place
React Query
re-derive, no refetch
Grids + map
updated in lockstep
Interactive maps

Hundreds 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.

Migration

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.

The stack

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.

Outcome

Desktop density, browser reach, real-time everything

30,000

Records per grid, windowed to ~120 mounted rows

300+

Real-time vehicle events a minute, reflected across every grid

SignalR
1

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.
Conclusion

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