Blog Docs Markets Portal Sign up
HomeBlog › Building a Live Odds Dashboard in React (with the Euro365 AP

Building a Live Odds Dashboard in React (with the Euro365 API)

Tutorial 27 May 2026 · 10 min read

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;
}

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>
  );
}

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} />;
}

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:

5. Upgrade to WebSocket later (not first)

Ship polling, measure, then consider WebSocket. We wrote about when WebSocket actually wins; 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. The Euro365 portal lets you create a domain-locked key for browser use if you really must — but server-proxy is still safer.
Get an API key Try the explorer