Skip to Content
All case studies

Cricktivity: Building a Simple, Offline-Resilient Cricket Scorer

A compact case study describing Cricktivity, a fast, offline-resilient cricket scoring app built with Next.js and React.

  • Next.js
  • React
  • TypeScript
  • Tailwind CSS
  • Radix UI
External LinkVisit the live project

Cricktivity is a browser-based cricket scoring app built for a practical use case: recording a local two-innings cricket match without needing a complex scoring platform, user accounts, or a reliable internet connection.

Its design is deliberately focused. A scorer opens the app, configures the match rules, records each delivery, and gets an automatically updated score, over count, run rate, target, and result. The app is less a live broadcast system than a fast, single-device scoring tool—one that continues to function when connectivity is unreliable.

The problem it solves

Cricket scoring seems simple until the edge cases arrive. A scorer must track runs, wickets, legal deliveries, overs, extras, targets, innings changes, and end-of-match conditions. The usual failure mode is not adding a run incorrectly; it is letting multiple pieces of state drift apart.

For example:

  • A wide adds a run but does not consume a legal ball.
  • A no-ball behaves similarly.
  • Byes and leg-byes count as legal deliveries.
  • A wicket can coincide with runs or an extra.
  • The end of an over must move its delivery record into history.
  • Undo must restore the entire prior scoring state, not only the visible total.
  • The chase ends either when the target is reached or when wickets/overs run out.

Cricktivity concentrates that complexity in one place: a React hook named useGameState.

Product flow

The match flow is intentionally direct.

First, the scorer can customize the match: total overs, wickets per innings, balls per over, and which scoring controls should be available. Team names are editable directly from the score display.

During an innings, the scorer records deliveries using run buttons, a wicket toggle, extra toggles for wides/no-balls/byes/leg-byes, and a custom-runs input. Each action updates the live scorecard immediately.

When the first innings finishes—either manually or because the configured over/wicket limit is reached—the app captures that innings, calculates a target of first-innings score plus one, and presents an innings-break screen. Starting the second innings resets the active score while retaining the first team’s summary.

The second innings ends automatically when the chasing team reaches the target, loses all wickets, or exhausts the available overs. Cricktivity then presents a winner or draw state, while still allowing the scorer to undo the final event if necessary.

The central design: model each scoring action as a delivery

The key implementation decision is to represent each action as a BallResult:

BallResult = runs + wicket flag + extra flag + optional extra type

Instead of scattering updates through the interface, the score buttons translate user interaction into a single scoring event. The game-state hook then interprets it according to cricket rules.

This gives the app one reliable path for:

  • increasing the score;
  • deciding whether a ball is legal;
  • increasing wickets;
  • completing an over;
  • adding a token to the “This Over” display;
  • updating the relevant team summary;
  • creating an undo checkpoint.

That design is especially valuable because it prevents the classic problem of incrementing balls in one function, wickets in another, and score in a third—an approach that tends to create hard-to-reproduce inconsistencies.

State management and undo

Cricktivity uses local React state rather than Redux, Zustand, or a server-side match engine. The useGameState hook holds a single GameState object containing the active innings, both teams’ summaries, match limits, current-over deliveries, prior completed overs, target score, innings phase, and match result.

Undo uses a snapshot approach. Before applying every scoring event, the app stores the prior relevant state in a history array. Pressing Undo restores the latest snapshot and clears a previously declared match result.

This is a pragmatic choice. For a local match scorer, snapshot history is easier to reason about than a reversible-event engine. It trades some memory efficiency for correctness and simplicity—a good exchange at this scale.

Handling cricket rules

The core scoring logic distinguishes legal balls from extras:

  • Wide / no-ball: add the selected runs plus the automatic penalty run; do not increment the legal-ball count.
  • Bye / leg-bye: add runs and consume a legal ball.
  • Ordinary run: add runs and consume a legal ball.
  • Wicket: increment wickets, capped at the match’s configured wicket limit.
  • Completed over: when legal balls reach ballsPerOver, reset the ball counter, increment overs, move the completed delivery list into allOvers, and clear the current-over list.

Run rate is calculated from actual balls faced:

run rate = score / (completed overs + balls / balls per over)

This matters because cricket notation such as 12.3 means twelve overs and three balls—not 12.3 decimal overs.

Offline-first, but not collaborative

Cricktivity’s offline strategy is simple and effective for its intended workflow.

The match state is persisted to browser localStorage, so a scorer can refresh the page or reopen the app without losing an in-progress match. The app also uses Serwist to provide Progressive Web App support: static resources are cached, the service worker claims clients promptly, and navigation falls back to an offline page when needed.

This makes Cricktivity resilient on a single device. It does not, however, implement multi-device synchronization.

Firebase is present, but it is not used to store or synchronize match state. It supports two peripheral features:

  • a global “like” count, updated through Firestore atomic increments;
  • feature-request submission.

There are no Firestore listeners, WebSockets, match IDs, shared scorecard URLs, or spectator views. In other words, the score is live in the local UI, not live across the internet.

Interface and implementation choices

The app is built with Next.js, React, TypeScript, Tailwind CSS, and Radix UI primitives. That combination makes sense for a compact interactive web application:

  • Next.js provides the application structure and PWA-friendly web deployment.
  • React handles interactive score controls and conditional match screens.
  • TypeScript makes the scoring state and delivery shapes explicit.
  • Tailwind keeps styling close to the interface.
  • Radix UI supplies accessible building blocks for settings sheets, dialogs, switches, tabs, and tooltips.

The interface avoids turning scoring into a form-heavy workflow. The scorer sees the team name, score, wickets, overs, run rate, target, current-over events, and scoring controls in one compact layout. The score buttons keep common actions one tap away, while less common configuration lives inside a settings sheet.

Development lessons visible in the history

The commit history shows that the difficult parts were not visual polish but scoring correctness.

Several fixes cluster around delivery and over accounting: incrementing balls correctly, representing extras, displaying mixed wicket/extra events, and ensuring completed overs are recorded correctly. Another set of fixes focuses on result logic, including draw handling, winner selection, and preventing repeated result-state updates once a match is complete.

A particularly useful lesson is that derived values should remain derived. An early run-rate bug appeared when undo restored the visible score state without restoring a separately maintained run-rate value. The later structure computes run rate from score, overs, and balls, making undo naturally correct.

Limits and next opportunities

Cricktivity is intentionally closer to a scoring pad than a full cricket platform. It does not yet include player-level batting and bowling statistics, strike rotation, partnerships, dismissal types, bowler figures, saved match archives, shareable scorecards, or collaboration.

The repository does show an interesting later, unmerged direction: team management with sortable player lists and a match-summary component capable of rendering a result image for download. Those experiments point naturally toward the next stage of the product: richer scorecards and post-match sharing.

The most valuable next architectural change would be separating the local scoring engine from persistence. The existing GameState and BallResult model could remain intact while a match repository layer adds Firestore-backed matches, real-time listeners, conflict handling, and public scorecard links.

Conclusion

Cricktivity succeeds because it treats cricket scoring as a state-management problem first and a UI problem second. Its most important idea is not a flashy feature—it is the decision to funnel every scoring action through one delivery model and one state engine.

That gives the app a clean foundation for local scoring: quick input, reliable undo, correct innings transitions, offline persistence, and straightforward result logic. It is a compact example of how careful modeling can make a deceptively complicated domain feel simple to the person using it.