DEV Community

Cover image for Building a Real-Time Cricket Scorecard Library in React: Handling High-Frequency WebSockets at 60 FPS
Ashish Verma
Ashish Verma

Posted on

Building a Real-Time Cricket Scorecard Library in React: Handling High-Frequency WebSockets at 60 FPS

The problem

Live cricket scores update fast — sometimes multiple events land in the same animation frame. Wire that straight into setState and you get wasted renders, visible jank, and a UI that quietly falls behind the real match. Most sports scoreboard code I found on GitHub doesn't handle this at all — it just re-renders on every socket message and hopes for the best.

I wanted to see what it actually takes to build one that doesn't do that.

What it does differently

Batches, doesn't just receive. Every incoming WebSocket packet gets pushed into a buffer, and a single requestAnimationFrame callback flushes the whole buffer into one state commit — right before the browser paints. Ten events arriving in the same frame become one render, not ten.

De-duplicates by ID. Reconnects happen. Servers replay events. A processedIds set (capped and FIFO-evicted so it can't leak memory) means a re-sent packet never gets applied twice — checked at push-time, not flush-time, so even two duplicates arriving in the same frame before a flush can't slip through.

Never trusts the feed blindly. A malformed packet gets caught and skipped, not left to crash the whole batch. Once an innings ends — 10 wickets, the overs limit, or a chased-down target — the ingestion engine ignores every further packet, so a feed that keeps sending late events after a match is over can't reopen it.

Gets the actual cricket rules right. This was the part that took longer than expected: standard 2.3-overs notation (not a decimal fraction — 2 completed overs, 3 balls into the next), a full second-innings target chase where reaching the target ends the match immediately, even mid-over, and an accurate result — win by runs, win by wickets, or a tie — computed from the actual match state rather than guessed.

Doesn't own anything it doesn't have to. It never opens the WebSocket connection — you bring your own socket, your own reconnect logic, your own auth. It never calls an AI provider either: there's a generateSummary prop that accepts any function (sync or async) and just renders whatever text comes back. Same philosophy both times — accept a result, don't own the mechanism.

Isolated by construction. Each <LiveCricketScorecard> gets its own store via React Context, created once with a lazy useState initializer (not useMemo, which turned out to have a real bug: it silently reset the entire match on every parent re-render if the caller didn't memoize initialData — which is the default case for most usage). Two scorecards on one page genuinely can't interfere with each other.

What's in it

  • rAF-batched ingestion with dedup and malformed-payload resilience
  • 10-wicket cap, configurable overs limit, full second-innings target chase
  • An onInningsComplete callback and an AI-extensibility hook (generateSummary)
  • aria-live regions for ball-by-ball updates and match results
  • 13 tests covering the batching, dedup, and cricket-rules logic
  • TypeScript, ESM + UMD builds, zero required CSS beyond Tailwind

Try it

npm install react-sports-scorecard
Enter fullscreen mode Exit fullscreen mode
  • Live demo — a simulated feed, a second-match spawner to see the isolation directly, and a real target chase you can play through
  • GitHub — the ingestion engine specifically is in src/ingestion/socketIngestion.ts if you want to see how the batching/dedup logic works
  • npm

Cricket-only for now, MIT licensed. Issues and PRs welcome — I'm particularly interested in what breaks under a real production feed, since everything here has only been tested against a simulated one.

Top comments (1)

Collapse
 
ashish_verma_0f878dafa2cf profile image
Ashish Verma

Going to do a deeper dive into the architecture and rAF batching logic on LinkedIn tomorrow!