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.
A two-column dashboard: left = list of live matches, right = the selected match's 1X2 + Over/Under 2.5 prices, updating every second.
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.
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>
);
}
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} />;
}
If you blindly setState on every fetch, every odds change re-renders the whole tree. Three quick wins:
const Row = React.memo(({ ev }) => ...)_id from Euro365's events endpoint is monotonically stable across the event's lifetime; don't synthesize keys from array index.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.