惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
大猫的无限游戏
大猫的无限游戏
月光博客
月光博客
Last Week in AI
Last Week in AI
腾讯CDC
The Cloudflare Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
MyScale Blog
MyScale Blog
博客园 - Franky
MongoDB | Blog
MongoDB | Blog
I
InfoQ
雷峰网
雷峰网
人人都是产品经理
人人都是产品经理
Blog — PlanetScale
Blog — PlanetScale
Y
Y Combinator Blog
H
Help Net Security
T
Tailwind CSS Blog
美团技术团队
aimingoo的专栏
aimingoo的专栏
博客园 - 三生石上(FineUI控件)
云风的 BLOG
云风的 BLOG
Google DeepMind News
Google DeepMind News
P
Proofpoint News Feed

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Building a Live Odds Dashboard in React (without the re-r...
dritancela.i · 2026-05-27 · via DEV Community

dritancela.it@gmail.com

A live odds dashboard is one of those projects that looks simple — fetch some JSON, display it, refresh on a timer — and then quietly becomes a re-render nightmare. Here's a working React 18+ implementation that doesn't grind your browser when 200 odds update in the same second.

What we're building

A two-column dashboard: left = list of live matches, right = the selected match's 1X2 + Over/Under 2.5 prices, updating every second.

1. The fetch hook (polling-based, cache-aware)

import { useEffect, useState, useRef } from 'react';

export function useLiveOdds(sportId = 1, intervalMs = 1500) {
  const [events, setEvents] = useState([]);
  const etagRef = useRef('');
  useEffect(() => {
    let alive = true;
    async function tick() {
      try {
        const r = await fetch(`https://api.euro365.bet/v1/events?sport=${sportId}&live=1`, {
          headers: { 'X-API-Key': import.meta.env.VITE_E365_KEY, 'If-None-Match': etagRef.current }
        });
        if (r.status === 304) return;            // nothing changed, no re-render
        etagRef.current = r.headers.get('etag') || '';
        const data = await r.json();
        if (alive) setEvents(data.events ?? []);
      } catch (_) { /* swallow; next tick will retry */ }
    }
    tick();
    const id = setInterval(tick, intervalMs);
    return () => { alive = false; clearInterval(id); };
  }, [sportId, intervalMs]);
  return events;
}

Enter fullscreen mode Exit fullscreen mode

Key detail: we send If-None-Match. The API answers 304 most of the time. No JSON parsing, no setState, no re-render. This single change cuts CPU by ~80% vs naive polling.

2. The match list

function MatchList({ onPick, picked }) {
  const events = useLiveOdds(1, 2000);  // 2s for the list — frequent enough
  return (
    <ul className="match-list">
      {events.map(ev => (
        <li key={ev._id}
            className={picked === ev._id ? 'on' : ''}
            onClick={() => onPick(ev._id)}>
          <span>{ev.h} vs {ev.a}</span>
          <small>{ev.score ?? '0:0'}</small>
        </li>
      ))}
    </ul>
  );
}

Enter fullscreen mode Exit fullscreen mode

3. The detail panel (faster polling, narrower payload)

function MatchDetail({ eventId }) {
  const [odds, setOdds] = useState({});
  useEffect(() => {
    if (!eventId) return;
    let alive = true;
    async function tick() {
      const r = await fetch(`https://api.euro365.bet/v1/odds?events=${eventId}&markets=1001,1018`, {
        headers: { 'X-API-Key': import.meta.env.VITE_E365_KEY }
      });
      const d = await r.json();
      if (alive) setOdds(d[eventId] ?? {});
    }
    tick();
    const id = setInterval(tick, 1000);  // 1s for the focused match
    return () => { alive = false; clearInterval(id); };
  }, [eventId]);
  if (!eventId) return <div className="empty">Pick a match</div>;
  return <OddsTable odds={odds} />;
}

Enter fullscreen mode Exit fullscreen mode

4. Stable rendering: avoid the 200-tick re-render storm

If you blindly setState on every fetch, every odds change re-renders the whole tree. Three quick wins:

  • memo the row components so unchanged matches don't repaint: const Row = React.memo(({ ev }) => ...)
  • shallow-compare before setState — if the new payload is structurally identical, skip the setState entirely.
  • use a stable key — the _id from the events endpoint is monotonically stable across the event's lifetime; don't synthesize keys from array index.

5. Upgrade to WebSocket later (not first)

Ship polling, measure, then consider WebSocket. For a dashboard with < 30 visible matches, polling at 1–2s is fine and easier to debug.

Production tip: never hardcode the API key in client-side React. Proxy through your own backend (Next.js API route, Lambda, whatever) so the key stays server-side. Most sports-data APIs let you create a domain-locked key for browser use if you really must — but server-proxy is still safer.


I built this against the Euro365 sports betting odds API — they have a free tier (100 req/min, no card required) which is plenty for prototyping a dashboard. The same patterns work against any odds API that supports ETag/Last-Modified on the events endpoint.

The original post lives at https://api.euro365.bet/blog/react-live-odds-dashboard/ — drop a comment if you've shipped something similar and hit a different gotcha.