React Nyheds Dashboard Tutorial 2026: SSE + TypeScript

Kent Hudson

Kent Hudson

·

21 mins Læs

React Nyheds Dashboard Tutorial 2026: SSE + TypeScript

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 Requests som en UX-tilstand: vis et "genopretter forbindelse om Xs"-banner med eksponentiel backoff (2 → 4 → 8 → 16 → 32 → 60 sekunder, samme kurve som EventSource bruger).
  • 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 fetch og EventSource — 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:

ScenarieOpdateringsfrekvensAnbefalet transportHvorfor
Afstemning<1 event / 30 ssetInterval + hentBillig, triviel, virker gennem enhver proxy
Server-Sent Events1-10 events / sek, kun server→klientEventSourceAutomatisk genforbindelse, HTTP/2 venlig, ingen broker
WebSocket>10 events / sek eller tovejsws + serverFuld-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:

  1. API nøglen sidder i query-strengen for SSE. Browsere kan ikke sætte brugerdefinerede headers på EventSource anmodninger. APITube accepterer både X-API-Key header (REST) og api_key query-param (SSE).
  2. Begræns in-memory bufferen til 100 artikler. Uden dette vil et travlt filter som language.code=en køre en fane uden for hukommelsen over en lang session.
  3. 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 EventSource bruger.
  • 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:

  1. Byt in-memory cachen ud med localStorage, så en genindlæsning af fanen ikke viser et tomt feed, mens SSE forbinder.
  2. Tilføj useSWR, hvis du vil have caching på tværs af komponenter gratis. useRateLimitedFetch hooken, der vises her, er bevidst afhængighedsfri, så du kan se mekanismen.
  3. Tilføj media[] rendering for miniaturebilleder af artikler — APITube returnerer dem i samme svar.
  4. Filtrer efter topics[] og entities[] for en NLP-drevet vertikal (finans, krypto, sport). APITube's entitetsekstraktion er indbygget.
  5. Byt EventSource ud 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.

APITube - News API

Relaterede artikler

Next.js Flersproget App-tutorial 2026: UI Indhold i18n</h4><h2></h2>
Developer Guides

Next.js Flersproget App-tutorial 2026: UI Indhold i18n</h4><h2></h2>

Build a Next.js 16 news reader with translated interface text, articles in 60+ languages, and metadata for each language. Includes next-intl, hreflang, a sitemap, and RTL support.

Bedste nyheder API til React 2026 (RSC, SWR, TanStack Query)
Insights

Bedste nyheder API til React 2026 (RSC, SWR, TanStack Query)

Bedste nyheder API til React i 2026 — 6-valg, Next.js 15 RSC-opskrift, useInfiniteQuery-paginering, SSE live feed og hvordan man ikke lækker sin API-nøgle.

Real-time news alerts with WebSockets (Socket.IO 2026)
Developer Guides

Real-time news alerts with WebSockets (Socket.IO 2026)

Build real-time news alerts with WebSockets in Node.js using Socket.IO 4.8 and APITube SSE. Room filters, reconnect, Redis adapter, full code.

How to Build a News Aggregator App in 30 Minutes (2026)
Developer Guides

How to Build a News Aggregator App in 30 Minutes (2026)

Learn how to build a news aggregator app in 30 minutes with vanilla JavaScript. Search, filters, rate-limit handling — full working code included.

Vi bruger cookies

Ved at klikke på "Accepter" accepterer du cookies på din enhed, så webstedet fungerer og til analyse.