A button people press 57 times per visit: building a no-backend tap counter that doesn't lose counts

2026-11-09来源 dev.toEnglish

I built a small chest counter for a Fortnite event: players keep it open beside the game and tap "+1 chest" every time they open one. Over the last three days the page got about 3,100 loads and about 178,000 taps on that one button, roughly 57 per visit.

It has no accounts and no login: the count is kept in the browser with localStorage. Here are the decisions that mattered when a single button gets hammered like that.

1. Keep the state logic pure

All the counting is a reducer with no DOM:

export function apply(state, action) { const s = { ...state }; switch (action.type) { case 'chest': s.chests = whole(s.chests + action.delta, MAX_CHESTS); break; case 'match': s.matches = whole(s.matches + action.delta, MAX_MATCHES); break; case 'set': { const n = whole(action.chests, MAX_CHESTS); if (n !== null) s.chests = n; break; } case 'reset': s.chests = 0; s.matches = 0; break; // (party size / "count my party" cases omitted here) default: break; } return s; } 

whole() floors the value and clamps it between 0 and a cap. That one helper kills a whole class of bugs: -1 on zero, someone pasting 1e999, or a "Set count" field holding "abc". Because it's pure, every edge case is a one-line unit test.

2. Never trust what comes back from localStorage

Whatever you saved last week may not be what you read today: an old schema, a half-written value, or a user editing it in DevTools. So loading goes through a parser that validates each field and falls back to an empty state instead of throwing:

export function parseState(raw) { let d; try { d = JSON.parse(raw); } catch { return emptyState(); } if (!d || typeof d !== 'object') return emptyState(); // ...validate chests / matches / party, else emptyState() } 

And every read and write is wrapped in try/catch. In some private modes localStorage.setItem throws; the counter should keep working for that session rather than die on the first tap.

3. A per-day log keyed by the player's local date

Players wanted "how many today", not just a season total. The daily log is a map of YYYY-MM-DD to { chests, matches }:

export function dayKey(d = new Date()) { return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; } 

Two details:

  • Use the local date, not toISOString(). A US player grinding at 9 PM would otherwise have their evening counted as "tomorrow" (UTC).
  • Cap the history (I keep the last 60 days) so the stored JSON can't grow forever.

Only taps go into the daily log. "Set count" (copying the total from the game) changes the season total but not today's number, or one paste would look like 2,000 chests in a day.

4. Make the button fast to hit

  • A keyboard shortcut (+) for people with the counter on a second monitor, ignored while typing in an input.
  • The screen stays on while counting (Wake Lock API; I wrote up the gotchas in a previous post).
  • Every tap re-renders from state, so there's no "the number and the storage disagree" bug.

What I'd add next

The honest gap is portability: the count lives in one browser, so switching phones loses it. An export link or a backup file is the obvious next step before anything with accounts.

If you want to see the result, here's the live counter. Press + on desktop.

原文:https://www.a8q9.cn/en/article/a-button-people-press-57-times-per-visit-building-a-no-backend-tap-counter-that--7b5071.html

相关文章