React News Dashboard Tutorial 2026: SSE + TypeScript

Kent Hudson

Kent Hudson

·

21 minuti Leggete

React News Dashboard Tutorial 2026: SSE + TypeScript

Build a Real-Time News Dashboard with React and APITube

Real-time news dashboards look simple until you actually ship one. You start with a 30-second polling loop, hit a 429 on your first Friday morning, add a retry, double the interval, and now the dashboard is neither real-time nor reliable.

This tutorial walks through a React news dashboard that does the three things most tutorials skip: chooses the right real-time transport for the actual workload, handles rate limits without silently breaking, and exposes the sentiment and category filters that make a news feed useful instead of noisy.

You'll use TypeScript, React 18 hooks, and the APITube News API (disclosure: I work on APITube). End state is a dashboard with a live-updating feed, filters, and a working deploy to Vercel. Full source on GitHub — link at the end.

This article is for React developers who know hooks and TypeScript basics and want a production-shaped template, not a toy demo.

Key Takeaways

  • Use Server-Sent Events for news feeds that update under 10 times per second — WebSocket is overkill and needs backend infrastructure you don't need.
  • Treat 429 Too Many Requests as a UX state: show a "reconnecting in Xs" banner with exponential backoff (2 → 4 → 8 → 16 → 32 → 60 seconds, the same curve EventSource uses).
  • Keep an in-memory buffer cap (100 articles) to avoid OOM over long sessions.
  • APITube's free tier (30 requests / 30 minutes + SSE access) is enough for production experimentation.
  • Full stack: React 18, TypeScript, Vite, Vercel — no Node server, no pub/sub broker, no paid SaaS.

1. What you'll build

A real-time news dashboard is a single-page web app that streams articles from a news API as they are published, filters them client-side, and updates without a page refresh. This tutorial builds one with three panels: a live feed of the latest articles, a category filter sidebar (technology, business, sports…), and a sentiment chip on each card (green / neutral / red). Articles stream in using Server-Sent Events — no page refresh, no polling loop, no backend broker.

The architecture is deliberately minimal: no Node server, no WebSocket broker, no Pusher account. The React app talks directly to APITube using the REST endpoint for the first render and the Server-Sent Events endpoint for live updates.

Stack:

  • React 18 + TypeScript (Vite)
  • Native fetch and EventSource — no client library
  • Two custom hooks: useRateLimitedFetch, useNewsStream
  • Vercel for deploy

2. Prerequisites

You need Node 20+, a working knowledge of React hooks (useState, useEffect, cleanup functions), and a free APITube API key. Sign-up is email-only, no card. The free tier gives you 30 requests per 30 minutes plus SSE access — enough for this build and for production experimentation.

Basic TypeScript is assumed (interfaces, generics, union types). No backend work required.

3. Polling vs SSE vs WebSocket: choosing real-time transport

Before any code, pick the right mechanism. Most React dashboards default to WebSocket because that's what the "real-time" articles rank for on Google, but for news data WebSocket is almost always overkill.

The decision framework:

ScenarioUpdate frequencyRecommended transportWhy
Polling<1 event / 30 ssetInterval + fetchCheap, trivial, works through any proxy
Server-Sent Events1-10 events / sec, server→client onlyEventSourceAuto-reconnect, HTTP/2 friendly, no broker
WebSocket>10 events / sec or bidirectionalws + serverFull-duplex but needs infra

News events arrive at most a few per second per filter. That puts them squarely in SSE territory. WebSocket is overkill — events arrive well under 10 per second, so SSE wins on complexity without sacrificing latency. Use it.

You'll still implement a polling fallback for the first render (so the feed isn't empty for 0.5 s while SSE connects) and for browsers that block EventSource behind aggressive proxies.

What's the best way to build a real-time feed in React?

The best way to build a real-time feed in React is Server-Sent Events for feeds that update under ten times per second, combined with an initial REST fetch for the first render. SSE reconnects automatically, works through proxies, and needs no server infrastructure when your data source already provides an SSE endpoint.

4. Project setup

Scaffold a Vite + React + TypeScript app:

npm create vite@latest news-dashboard -- --template react-ts
cd news-dashboard
npm install
npm install --save-dev @types/node

Create a .env.local in the project root:

VITE_APITUBE_API_KEY=your_key_here

Project structure (src/ directory):

src/
  hooks/
    useRateLimitedFetch.ts
    useNewsStream.ts
  components/
    NewsCard.tsx
    CategoryFilter.tsx
    NewsDashboard.tsx
  types/
    news.ts
  lib/
    apitube.ts
  App.tsx
  main.tsx

How do I structure a React dashboard project?

Structure a React dashboard project around data concerns, not page layout. Put fetch logic in hooks/, types in types/, API clients in lib/, and dumb presentational components in components/. Every component that renders a request result should get its data from a single typed hook — never call fetch inline. This keeps caching, retries, and rate-limit logic in one place.

5. Fetching news with the APITube API

Start with the REST endpoint. Define the response type once and reuse it everywhere.

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[];
  meta: { page: number; per_page: number; total: 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 authenticates with the X-API-Key header (not a bearer token — a common footgun when migrating from other news APIs). The response shape is stable across /v1/news/everything and the SSE stream, so the same NewsArticle type covers both.

How do I fetch news data in a React app?

Fetch news data in a React app with a typed wrapper around the news API plus a custom hook that owns loading and error state. Always render from a single hook per data source — never call fetch directly in a component — so rate-limit handling, caching, and auth live in one place. Pass URLSearchParams into the wrapper to keep query construction type-safe.

6. Going real-time with Server-Sent Events

APITube exposes an SSE endpoint at /v1/news/integrations/sse-stream that pushes articles as they're ingested, filtered by the same query parameters as the REST endpoint. The browser's native EventSource handles the connection, reconnection, and backoff — no library needed.

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

Three details that matter in production:

  1. The API key rides in the query string for SSE. Browsers can't set custom headers on EventSource requests. APITube accepts both X-API-Key header (REST) and api_key query param (SSE).
  2. Cap the in-memory buffer at 100 articles. Without this, a busy filter like language.code=en will OOM a tab over a long session.
  3. Return a cleanup function that closes the EventSource. React StrictMode will mount-unmount-remount in dev — without cleanup you'll open two connections and burn your rate limit.

How do I use Server-Sent Events in React?

Use Server-Sent Events in React by creating an EventSource inside a useEffect, updating state in the onmessage handler, and returning a cleanup function that calls source.close(). The browser handles reconnection automatically with exponential backoff. Pass query parameters to filter the stream server-side so you don't transfer articles the UI will discard.

7. Handling rate limits: backoff and stale cache

The free tier is 30 requests per 30 minutes. The SSE stream doesn't count against that — but the REST fallback, initial page load, and filter changes do. Hit the limit and APITube returns 429 Too Many Requests.

Rate limits are a UX problem, not a backend one. When a dashboard hits 429 Too Many Requests, the user needs to know the feed is frozen and for how long — not stare at silently-stale data. Show a banner, count down, and serve the last-known-good cache.

Most tutorials silently retry on 429. That pattern is fine for a background job. For a live dashboard it breaks the contract: the user sees "fresh" timestamps on data that's minutes old.

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

The hook does three things you won't get from a naïve fetch:

  • Stale-while-revalidate: if a cached response exists for the same query, it renders immediately while a fresh fetch runs in the background.
  • Exponential backoff capped at 60 s: 2 → 4 → 8 → 16 → 32 → 60. This is the same curve the browser's native EventSource uses.
  • Exposes retryIn so the UI can render a "Reconnecting in 12s…" banner. The user knows what's happening. They don't close the tab.

How do I handle API rate limits in a React app?

Handle API rate limits in a React app with exponential backoff on 429 responses plus a stale-while-revalidate cache. Cache the last successful response per query key, render it immediately when the hook re-runs, and expose a retryIn counter so the UI can show a "reconnecting in Xs" banner instead of silently retrying. Treat 429 as a user-facing state, not a backend detail.

8. Filtering by category, language, and sentiment

The dashboard wouldn't be a dashboard without filters. APITube returns structured categories, language, and — uniquely — a sentiment object with overall, title, and body scores on every article.

Sentiment filtering matters more than it sounds. A crypto dashboard, a brand-monitoring tool, and a finance-news app all need to surface negative signal fast. Most news APIs hand you a raw text blob and leave scoring to you.

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 is a thin presentational component — just the title, source, timestamp, and a sentiment chip colored green / gray / red based on article.sentiment?.overall.polarity. Full source in the repo.

9. Deploying to Vercel

The whole app is static + client-side — no server runtime, no API routes. Vercel handles it with a single command.

npm install -g vercel
vercel

Environment variable setup:

Vite prefixes client-exposed vars with VITE_. That means your API key ships in the bundle and runs against APITube from the user's browser. For a personal dashboard or an authenticated admin panel that's fine. For a public marketing site, proxy through a serverless function instead.

Add the key in the Vercel dashboard under Project Settings → Environment Variables:

VITE_APITUBE_API_KEY=<your_key>

CORS: APITube allows any origin on both the REST and SSE endpoints when the request includes a valid API key, so no CORS configuration is needed on your side. If you switch to a proxy architecture, set Access-Control-Allow-Origin on your Vercel function.

Deploy and you'll have a working react news dashboard at <project>.vercel.app in under a minute.

10. Where to go next

The build works. The obvious extensions:

  1. Swap the in-memory cache for localStorage so a tab reload doesn't show an empty feed while the SSE connects.
  2. Add useSWR if you want caching across components for free. The useRateLimitedFetch hook shown here is intentionally dependency-free so you can see the mechanism.
  3. Add media[] rendering for article thumbnails — APITube returns them in the same response.
  4. Filter by topics[] and entities[] for an NLP-powered vertical (finance, crypto, sports). APITube's entity extraction is built in.
  5. Swap EventSource for @microsoft/fetch-event-source if you need custom headers on the stream connection.

Full source: github.com/apitube/react-news-dashboard-tutorial

Frequently Asked Questions

Does this work with React Server Components?

Partially. The fetch hook runs fine on the client. EventSource is browser-only — you can't await a stream in an RSC. Render the dashboard as a Client Component ('use client') and you're good.

Can I use this pattern with a different news API?

Yes. Swap src/lib/apitube.ts for your provider's wrapper and update NewsArticle to match their response shape. The hook pattern — useRateLimitedFetch + useNewsStream — is provider-agnostic. Note that most news APIs don't expose an SSE endpoint, so you'll fall back to polling every 30-60 s.

How do I avoid blowing through the free tier in development?

Cache aggressively during dev — set staleTime: Infinity in your cache layer and refresh manually. Also guard the SSE connection with an enabled flag so it doesn't open on every hot reload.

Does SSE work on Safari?

Yes, since Safari 5. All modern browsers support EventSource. The one quirk is that Safari will silently close the connection after ~60 s of idle — APITube sends a keep-alive comment every 30 s to prevent this.

APITube - News API

Articoli correlati

Best Financial News API for Trading 2026: 5 Compared
Insights

Best Financial News API for Trading 2026: 5 Compared

Five financial news APIs scored on latency, ticker-tagging, sentiment, backtesting archive, and trading-event feeds. 2026 fintech-focused comparison.

NewsAPI.org Alternative 2026: Why Devs Pick APITube
Insights

NewsAPI.org Alternative 2026: Why Devs Pick APITube

NewsAPI.org alternative for 2026 — TOS quote, real migration code, 12-month TCO, and when NewsAPI is still fine. APITube vs NewsAPI.org, straight.

How to Scale a News App to Millions of Users (2026 Architecture Guide)
Insights

How to Scale a News App to Millions of Users (2026 Architecture Guide)

Spike-driven traffic, freshness vs cache trade-offs, autoscaling thresholds that actually fit news workloads, a TTL matrix, build-vs-buy cost math from 100K to 100M MAU, and a reference stack. With working ingestion code.

Come iniziare con APITube News API: una guida passo-passo
Product

Come iniziare con APITube News API: una guida passo-passo

Scopri come utilizzare APITube News API per accedere ai dati delle notizie in tempo reale da oltre 500.000 fonti verificate in tutto il mondo. Questa guida copre tutto, dalla registrazione alle opzioni di filtraggio avanzate.

Utilizziamo i cookie

Facendo clic su "Accetta", accetti la memorizzazione dei cookie sul tuo dispositivo per funzionalità e analisi.