Byg et realtids nyheds-dashboard med React og APITube
Realtids nyheds-dashboards ser simple ud, indtil du leverer et. Du starter med en 30-sekunders polling-loop, rammer en 429 din første fredag morgen, tilføjer en retry, fordobler intervallet, og nu er dashboardet hverken realtid eller pålideligt.
Denne vejledning gennemgår et React nyheds-dashboard, der håndterer rate limits uden at gå i stykker lydløst og eksponerer sentiment og category filtrene, der gør et nyhedsfeed brugbart i stedet for støjende.
Du vil bruge TypeScript, React 18 hooks og APITube News API (oplysning: jeg arbejder på APITube). Slutresultatet er et dashboard med et live-opdaterende feed, filtre og en fungerende deploy til Vercel. Fuld kildekode på GitHub — link til sidst.
Denne artikel er til React-udviklere, der kender hooks og TypeScript-grundlæggende og ønsker en produktionsklar skabelon, ikke en legetøjsdemo.
Vigtigste pointer
- Brug Server-Sent Events til nyhedsfeeds, der opdateres op til 10 gange pr. sekund — WebSocket er overkill og kræver backend-infrastruktur, du ikke behøver.
- Behandl
429 Too Many Requestssom en UX-tilstand: vis et "genopretter forbindelse om Xs"-banner med eksponentiel backoff (2 → 4 → 8 → 16 → 32 → 60 sekunder, samme kurve somEventSourcebruger). - Behold en in-memory buffergrænse (100 artikler) for at undgå at løbe tør for hukommelse over lange sessioner.
- APITube's gratis niveau (100 anmodninger pr. dag + SSE adgang) er nok til produktions-eksperimentering.
- Fuld stack: React 18, TypeScript, Vite, Vercel — ingen Node-server, ingen pub/sub-broker, ingen betalt SaaS.
1. Hvad du vil bygge
Et realtids-nyhedsdashboard er en single-page web-app, der streamer artikler fra et nyheds API, når de udgives, filtrerer dem på klientsiden og opdaterer uden sideopdatering. Denne vejledning bygger en med tre paneler: et live-feed af de seneste artikler, en kategorifilterliste (teknologi, forretning, sport…) og et sentimentchip på hvert kort (grønt / neutralt / rødt). Artikler streames ind ved hjælp af Server-Sent Events — ingen sideopdatering, ingen polling-loop, ingen backend-mægler.
Arkitekturen er minimal: ingen Node-server, ingen WebSocket-broker, ingen Pusher-konto. React-appen taler direkte til APITube ved hjælp af REST-endepunktet til den første rendering og Server-Sent Events-endepunktet til live-opdateringer.
Stack:
- React 18 + TypeScript (Vite)
- Nativ
fetchogEventSource— intet klientbibliotek - To brugerdefinerede hooks:
useRateLimitedFetch,useNewsStream - Vercel til implementering
2. Forudsætninger
Du skal bruge Node 20+, praktisk kendskab til React hooks (useState, useEffect, cleanup-funktioner) og en gratis APITube API-nøgle. Tilmelding kræver kun e-mail, intet kort. Frit-planen giver dig 100 forespørgsler om dagen plus SSE-adgang — nok til dette build og til produktionseksperimenter.
Grundlæggende TypeScript antages (interfaces, generics, union types). Intet backend-arbejde kræves.
3. Polling vs SSE vs WebSocket: valg af realtidstransport
Før enhver kode, vælg den rigtige mekanisme. De fleste React dashboards bruger som standard WebSocket, fordi det er det, "realtids"-artiklerne rangerer for på Google, men for nyhedsdata er WebSocket næsten altid overkill.
Beslutningsrammen:
| Scenarie | Opdateringsfrekvens | Anbefalet transport | Hvorfor |
|---|---|---|---|
| Afstemning | <1 event / 30 s | setInterval + hent | Billig, triviel, virker gennem enhver proxy |
| Server-Sent Events | 1-10 events / sek, kun server→klient | EventSource | Automatisk genforbindelse, HTTP/2 venlig, ingen broker |
| WebSocket | >10 events / sek eller tovejs | ws + server | Fuld-dupleks men kræver infrastruktur |
Nyhedsbegivenheder ankommer med højst et par stykker i sekundet pr. filter. Det placerer dem solidt i SSE-territorium. WebSocket er overkill — begivenheder ankommer langt under 10 pr. sekund, så SSE vinder på kompleksitet uden at ofre latenstid.
Du vil stadig implementere en polling-fallback til den første rendering (så feedet ikke er tomt i 0.5 sekunder, mens SSE opretter forbindelse) og til browsere, der blokerer EventSource bag aggressive proxier.
Hvad er den bedste måde at bygge et realtidsfeed på i React?
Den bedste måde at bygge et realtidsfeed i React er Server-Sent Events for feeds, der opdateres mindre end ti gange i sekundet, kombineret med en indledende REST hentning til den første rendering. SSE genopretter automatisk forbindelsen, virker gennem proxies og kræver ingen serverinfrastruktur, når din datakilde allerede leverer en SSE endpoint.
4. Projektopsætning
Skaf en Vite + React + TypeScript app:
npm create vite@latest news-dashboard -- --template react-ts
cd news-dashboard
npm install
npm install --save-dev @types/node
Opret en .env.local i projektets rod:
VITE_APITUBE_API_KEY=your_key_here
Projektstruktur (src/ mappe):
src/
hooks/
useRateLimitedFetch.ts
useNewsStream.ts
components/
NewsCard.tsx
CategoryFilter.tsx
NewsDashboard.tsx
types/
news.ts
lib/
apitube.ts
App.tsx
main.tsx
Hvordan strukturerer jeg et React dashboard-projekt?
Strukturer et React dashboard-projekt omkring dataproblemer, ikke side-layout. Placer hentningslogik i hooks/, typer i types/, API-klienter i lib/ og præsentationskomponenter i components/. Hver komponent, der renderer et anmodningsresultat, skal få sine data fra en enkelt typet hook — kald aldrig fetch inline. Dette holder caching, genforsøg og rate-limit logik samlet.
5. Henter nyheder med APITube API
Start med REST-endpointet. Definer responstypen én gang og genbrug den overalt.
src/types/news.ts:
export interface NewsArticle {
id: string;
title: string;
description: string;
href: string;
published_at: string;
language: { code: string };
author?: { name: string };
categories?: { id: string; name: string }[];
sentiment?: {
overall: { score: number; polarity: 'positive' | 'neutral' | 'negative' };
title: { score: number };
body: { score: number };
};
source?: { domain: string; country?: string };
}
export interface NewsResponse {
status: string;
results: NewsArticle[];
has_next_pages: boolean;
page: number;
per_page: number;
}
src/lib/apitube.ts:
const BASE_URL = 'https://api.apitube.io';
const API_KEY = import.meta.env.VITE_APITUBE_API_KEY;
export async function fetchNews(params: URLSearchParams): Promise<NewsResponse> {
const res = await fetch(`${BASE_URL}/v1/news/everything?${params}`, {
headers: { 'X-API-Key': API_KEY },
});
if (!res.ok) throw { status: res.status, message: res.statusText };
return res.json();
}
APITube godkender med X-API-Key-headeren (ikke en bearer token — en almindelig fejl ved migrering fra andre nyheds-API'er). Svarets struktur er stabil på tværs af /v1/news/everything og SSE-streamen, så den samme NewsArticle-type dækker begge.
Hvordan henter jeg nyhedsdata i en React-app?
Hent nyhedsdata i en React-app med en typet wrapper omkring nyheds-API plus en brugerdefineret hook, der ejer loading- og fejlstatus. Render altid fra en enkelt hook pr. datakilde — kald aldrig fetch direkte i en komponent — så rate-limit-håndtering, caching og auth lever ét sted. Send URLSearchParams ind i wrapperen for at holde forespørgselskonstruktion typesikker.
6. Går i realtid med Server-Sent Events
APITube's SSE-endepunkt ved /v1/news/stream sender nye artikler, der matcher de samme filtre som REST-endepunktet. Browserens EventSource håndterer forbindelsen, genforbindelsen og backoff uden et ekstra bibliotek.
src/hooks/useNewsStream.ts:
import { useEffect, useState } from 'react';
import type { NewsArticle } from '../types/news';
interface StreamOptions {
category?: string;
language?: string;
enabled?: boolean;
}
export function useNewsStream(opts: StreamOptions) {
const [articles, setArticles] = useState<NewsArticle[]>([]);
const [status, setStatus] = useState<'connecting' | 'open' | 'closed'>('closed');
useEffect(() => {
if (!opts.enabled) return;
const params = new URLSearchParams({
'api_key': import.meta.env.VITE_APITUBE_API_KEY,
'language.code': opts.language ?? 'en',
});
if (opts.category) params.set('category.id', opts.category);
const url = `https://api.apitube.io/v1/news/stream?${params}`;
const source = new EventSource(url);
setStatus('connecting');
source.onopen = () => setStatus('open');
source.onmessage = (event) => {
const article = JSON.parse(event.data) as NewsArticle;
setArticles((prev) => [article, ...prev].slice(0, 100));
};
source.onerror = () => setStatus('closed');
return () => {
source.close();
setStatus('closed');
};
}, [opts.category, opts.language, opts.enabled]);
return { articles, status };
}
Tre detaljer, der betyder noget i produktion:
- API nøglen sidder i query-strengen for SSE. Browsere kan ikke sætte brugerdefinerede headers på
EventSourceanmodninger. APITube accepterer bådeX-API-Keyheader (REST) ogapi_keyquery-param (SSE). - Begræns in-memory bufferen til 100 artikler. Uden dette vil et travlt filter som
language.code=enkøre en fane uden for hukommelsen over en lang session. - Returner en oprydningsfunktion, der lukker
EventSource. React StrictMode vil montere-afmontere-remontere i dev — uden oprydning åbner du to forbindelser og brænder din rate limit.
Hvordan bruger jeg Server-Sent Events i React?
Brug Server-Sent Events i React ved at oprette en EventSource inde i en useEffect, opdatere tilstand i onmessage-handleren og returnere en oprydningsfunktion, der kalder source.close(). Browseren håndterer genforbindelse automatisk med eksponentiel backoff. Send forespørgselsparametre for at filtrere streamen på serversiden, så du ikke overfører artikler, som UI'en vil kassere.
7. Håndtering af rate limits: backoff og forældet cache
Gratis niveauet er 100 anmodninger pr. dag. SSE stream tæller ikke med i det – men REST fallback, indledende sideindlæsning og filterændringer gør. Ram grænsen, og APITube returnerer 429 Too Many Requests.
Rate limits er et UX-problem, ikke et backend-problem. Når et dashboard rammer
429 Too Many Requests, skal brugeren vide, at feedet er frosset, og hvor længe — ikke stirre på stille og roligt forældede data. Vis et banner, tæl ned, og server den sidst kendte gode cache.
De fleste tutorials genforsøger lydløst ved 429. Det mønster er fint for et baggrundsjob. For et live dashboard bryder det kontrakten: brugeren ser "friske" tidsstempler på data, der er minutter gamle.
src/hooks/useRateLimitedFetch.ts:
import { useEffect, useRef, useState } from 'react';
import { fetchNews } from '../lib/apitube';
import type { NewsResponse } from '../types/news';
interface State {
data: NewsResponse | null;
error: Error | null;
retryIn: number; // seconds until next attempt, 0 if idle
isStale: boolean; // true when showing cached data during backoff
}
export function useRateLimitedFetch(params: URLSearchParams) {
const [state, setState] = useState<State>({
data: null, error: null, retryIn: 0, isStale: false,
});
const cacheRef = useRef<Map<string, NewsResponse>>(new Map());
useEffect(() => {
const key = params.toString();
let cancelled = false;
let attempt = 0;
async function run() {
// Serve stale cache immediately if we have it.
const cached = cacheRef.current.get(key);
if (cached) setState((s) => ({ ...s, data: cached, isStale: true }));
while (!cancelled) {
try {
const data = await fetchNews(params);
cacheRef.current.set(key, data);
setState({ data, error: null, retryIn: 0, isStale: false });
return;
} catch (err: any) {
if (err.status !== 429) {
setState((s) => ({ ...s, error: err }));
return;
}
// Exponential backoff: 2, 4, 8, 16, 32, 60 (capped) seconds.
const delay = Math.min(2 ** (attempt + 1), 60);
attempt += 1;
for (let i = delay; i > 0 && !cancelled; i--) {
setState((s) => ({ ...s, retryIn: i, isStale: true }));
await new Promise((r) => setTimeout(r, 1000));
}
}
}
}
run();
return () => { cancelled = true; };
}, [params.toString()]);
return state;
}
Hook'en gør tre ting, som du ikke får fra en naiv fetch:
- Stale-while-revalidate: hvis et cachede svar eksisterer for den samme forespørgsel, renderes det øjeblikkeligt, mens en frisk hentning kører i baggrunden.
- Eksponentiel backoff begrænset til 60 s: 2 → 4 → 8 → 16 → 32 → 60. Dette er den samme kurve, som browserens native
EventSourcebruger. - Eksponerer
retryIn, så UI'en kan rendere et "Genopretter forbindelse om 12 sekunder..." banner. Brugeren ved, hvad der sker. De lukker ikke fanen.
Hvordan håndterer jeg API rate limits i en React-app?
Håndter API rate limits i en React app med eksponentiel backoff på 429 svar plus en stale-while-revalidate cache. Cache det sidste succesfulde svar pr. forespørgselsnøgle, render det øjeblikkeligt, når hooken kører igen, og eksponer en retryIn tæller, så UI'en kan vise et "genopretter forbindelse om Xs" banner i stedet for lydløst at prøve igen. Behandl 429 som en bruger-vendt status, ikke en backend-detalje.
8. Filtrering efter kategori, sprog og sentiment
Dashboardet ville ikke være et dashboard uden filtre. APITube returnerer struktureret categories, language og – unikt – et sentiment objekt med overall, title og body scorer på hver artikel.
Sentiment-filtrering betyder mere, end det lyder. Et kryptodashboard, et brandovervågningsværktøj og en finansnyheds-app skal alle hurtigt finde negative signaler. De fleste nyheds-API'er giver dig en rå tekstblok og overlader scoringen til dig.
src/components/NewsDashboard.tsx:
import { useMemo, useState } from 'react';
import { useRateLimitedFetch } from '../hooks/useRateLimitedFetch';
import { useNewsStream } from '../hooks/useNewsStream';
import { NewsCard } from './NewsCard';
import { CategoryFilter } from './CategoryFilter';
export function NewsDashboard() {
const [category, setCategory] = useState<string | undefined>();
const [sentimentFilter, setSentimentFilter] =
useState<'all' | 'positive' | 'negative'>('all');
const params = useMemo(() => {
const p = new URLSearchParams({ 'language.code': 'en', per_page: '20' });
if (category) p.set('category.id', category);
return p;
}, [category]);
const initial = useRateLimitedFetch(params);
const stream = useNewsStream({ category, language: 'en', enabled: true });
const articles = useMemo(() => {
const combined = [...stream.articles, ...(initial.data?.results ?? [])];
if (sentimentFilter === 'all') return combined;
return combined.filter(
(a) => a.sentiment?.overall.polarity === sentimentFilter
);
}, [stream.articles, initial.data, sentimentFilter]);
return (
<div className="dashboard">
<aside>
<CategoryFilter value={category} onChange={setCategory} />
<select
value={sentimentFilter}
onChange={(e) => setSentimentFilter(e.target.value as any)}
>
<option value="all">All sentiment</option>
<option value="positive">Positive</option>
<option value="negative">Negative</option>
</select>
</aside>
<main>
{initial.retryIn > 0 && (
<div className="banner">
Rate-limited — reconnecting in {initial.retryIn}s
{initial.isStale && ' (showing cached results)'}
</div>
)}
<div className="status">Stream: {stream.status}</div>
{articles.map((a) => <NewsCard key={a.id} article={a} />)}
</main>
</div>
);
}
NewsCard er en tynd præsentationskomponent — kun titlen, kilden, tidsstemplet og en sentimentchip farvet grøn / grå / rød baseret på article.sentiment?.overall.polarity. Fuld kilde i repoet.
9. Implementering til Vercel
Hele appen er statisk + klient-side – ingen server-runtime, ingen API-ruter. Vercel håndterer det med en enkelt kommando.
npm install -g vercel
vercel
Miljøvariabelopsætning:
Vite præfikserer klient-eksponerede variabler med VITE_. Det betyder, at din API-nøgle sendes i bundtet og kører mod APITube fra brugerens browser. For et personligt dashboard eller et autentificeret admin-panel er det fint. For et offentligt marketingwebsted, proxy via en serverløs funktion i stedet.
Tilføj nøglen i Vercel-dashboardet under Projektindstillinger → Miljøvariabler:
VITE_APITUBE_API_KEY=<your_key>
CORS: APITube tillader enhver oprindelse på både REST og SSE endepunkterne, når anmodningen indeholder en gyldig API nøgle, så ingen CORS konfiguration er nødvendig på din side. Hvis du skifter til en proxy-arkitektur, skal du indstille Access-Control-Allow-Origin på din Vercel-funktion.
Implementer, og du vil have et fungerende react nyheds-dashboard ved <project>.vercel.app på under et minut.
10. Hvor skal du hen nu
Builden virker. Udvidelsespunkter:
- Byt in-memory cachen ud med
localStorage, så en genindlæsning af fanen ikke viser et tomt feed, mens SSE forbinder. - Tilføj
useSWR, hvis du vil have caching på tværs af komponenter gratis.useRateLimitedFetchhooken, der vises her, er bevidst afhængighedsfri, så du kan se mekanismen. - Tilføj
media[]rendering for miniaturebilleder af artikler — APITube returnerer dem i samme svar. - Filtrer efter
topics[]ogentities[]for en NLP-drevet vertikal (finans, krypto, sport). APITube's entitetsekstraktion er indbygget. - Byt
EventSourceud med@microsoft/fetch-event-source, hvis du har brug for brugerdefinerede headers på stream-forbindelsen.
Fuld kilde: github.com/apitube/react-news-dashboard-tutorial
Ofte stillede spørgsmål
Fungerer dette med React Server Components?
Delvist. Fetch-hooken kører fint på klienten. EventSource er kun til browseren — du kan ikke await en stream i en RSC. Render dashboardet som en Client Component ('use client'), og du er klar.
Kan jeg bruge dette mønster med en anden nyheds-API?
Ja. Udskift src/lib/apitube.ts med din udbyders wrapper og opdater NewsArticle til at matche deres svarsform. Hook-mønsteret — useRateLimitedFetch + useNewsStream — er udbyder-agnostisk. Bemærk, at de fleste nyheds-API'er ikke eksponerer en SSE-endpoint, så du vil falde tilbage til polling hvert 30-60 sekund.
Hvordan undgår jeg at bruge hele gratis-niveauet under udvikling?
Cache aggressivt under udvikling — sæt staleTime: Infinity i dit cachelag og opdater manuelt. Beskyt også SSE-forbindelsen med et enabled-flag, så den ikke åbner ved hver hot reload.
Fungerer SSE på Safari?
Ja, siden Safari 5. Alle moderne browsere understøtter EventSource. Den eneste særhed er, at Safari lydløst lukker forbindelsen efter ca. 60 sekunder med inaktivitet — APITube sender en keep-alive kommentar hvert 30 sekund for at forhindre dette.


