Skip to content
All Insights
Mar 20268 min read

How to Build a Real-Time Analytics Dashboard with Next.js

A complete guide to building a real-time analytics dashboard using Next.js App Router, Server-Sent Events, and React Query. We'll cover data fetching, state management, and creating a smooth user experience.

Gabriel Njoabozia
Gabriel NjoaboziaFounder & Lead Engineer
Real-time analytics dashboard interface showing charts, metrics, and data visualizations

Introduction

Real-time analytics dashboards are one of the most common frontend challenges. They aggregate data from multiple sources, display live metrics, and help users make informed decisions. Building one well requires careful attention to data freshness, performance, and user experience.

In this guide, we'll build a complete analytics dashboard using:

  • Next.js with App Router
  • Server-Sent Events for real-time data
  • React Query for server state management
  • Recharts for interactive charts

Setting Up the Project

First, let's set up a new Next.js project with the required dependencies.

npx create-next-app@latest analytics-dashboard --typescript --tailwind
cd analytics-dashboard
npm install @tanstack/react-query recharts date-fns

Setting Up React Query

React Query provides composable hooks for server state. Let's configure it with a provider and client that handles auto-refetching.

// app/providers.tsx
"use client"

import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { useState } from "react"

export function Providers({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(
    () =>
      new QueryClient({
        defaultOptions: {
          queries: {
            staleTime: 30 * 1000, // 30 seconds
            refetchInterval: 60 * 1000, // Auto-refresh every minute
            retry: 2,
            refetchOnWindowFocus: true,
          },
        },
      })
  )

  return (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  )
}

Building the Metrics Dashboard

Here's how we structure the main dashboard with parallel data fetching using React Query, reactive chart updates, and proper loading states.

// hooks/useAnalytics.ts
import { useQuery } from "@tanstack/react-query"

async function fetchMetrics() {
  const res = await fetch("/api/metrics")
  if (!res.ok) throw new Error("Failed to fetch metrics")
  return res.json()
}

export function useMetrics() {
  return useQuery({
    queryKey: ["metrics"],
    queryFn: fetchMetrics,
  })
}

export function useRevenueData(timeframe: "7d" | "30d" | "90d") {
  return useQuery({
    queryKey: ["revenue", timeframe],
    queryFn: async () => {
      const res = await fetch(`/api/revenue?timeframe=${timeframe}`)
      return res.json()
    },
  })
}

export function useUserGrowth() {
  return useQuery({
    queryKey: ["user-growth"],
    queryFn: async () => {
      const res = await fetch("/api/users/growth")
      return res.json()
    },
  })
}

Server-Sent Events for Live Data

For truly real-time metrics, SSE is simpler than WebSockets and works over standard HTTP. Here's our approach:

// app/api/metrics/stream/route.ts
export async function GET(req: Request) {
  const encoder = new TextEncoder()
  const stream = new ReadableStream({
    start(controller) {
      // Send initial data
      const sendMetrics = async () => {
        const metrics = await getLatestMetrics()
        const data = `data: ${JSON.stringify(metrics)}\n\n`
        controller.enqueue(encoder.encode(data))
      }

      sendMetrics()

      // Push updates every 5 seconds
      const interval = setInterval(sendMetrics, 5000)

      req.signal.addEventListener("abort", () => {
        clearInterval(interval)
        controller.close()
      })
    },
  })

  return new Response(stream, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      Connection: "keep-alive",
    },
  })
}

// hooks/useSSE.ts: React hook for consuming SSE
export function useSSEMetrics() {
  const [data, setData] = useState<Metrics | null>(null)

  useEffect(() => {
    const eventSource = new EventSource("/api/metrics/stream")

    eventSource.onmessage = (event) => {
      const parsed = JSON.parse(event.data)
      setData(parsed)
    }

    eventSource.onerror = () => {
      // Reconnect logic: EventSource reconnects automatically
      console.warn("SSE connection error, retrying...")
    }

    return () => eventSource.close()
  }, [])

  return data
}

The Dashboard Layout

Putting it all together into a responsive dashboard with stat cards, charts, and a data table.

// app/dashboard/page.tsx
import { Suspense } from "react"
import { StatsGrid } from "./StatsGrid"
import { RevenueChart } from "./RevenueChart"
import { UserGrowthChart } from "./UserGrowthChart"
import { RecentActivity } from "./RecentActivity"

export default function DashboardPage() {
  return (
    <div className="space-y-6 p-6">
      <div>
        <h1 className="text-2xl font-bold">Analytics Dashboard</h1>
        <p className="text-white/60">
          Real-time metrics for your business
        </p>
      </div>

      <Suspense fallback={<StatsGridSkeleton />}>
        <StatsGrid />
      </Suspense>

      <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
        <Suspense fallback={<ChartSkeleton />}>
          <RevenueChart />
        </Suspense>
        <Suspense fallback={<ChartSkeleton />}>
          <UserGrowthChart />
        </Suspense>
      </div>

      <Suspense fallback={<TableSkeleton />}>
        <RecentActivity />
      </Suspense>
    </div>
  )
}

Handling Real-Time Updates

The key challenge with real-time data is preventing unnecessary re-renders. Here's how we approach it:

  • React Query for polling: Set refetchInterval for metrics that change slowly
  • SSE for live streams: Event counts, active users, revenue ticks
  • Optimistic updates: Update charts immediately and reconcile with server
  • Memoized selectors: Prevent chart re-renders when unrelated data changes

Performance Optimizations

Analytics dashboards can be expensive to render. These optimizations made the biggest difference for us:

  • Virtualized data tables for large datasets (>1000 rows)
  • Throttled chart updates (max 1 update per second regardless of SSE frequency)
  • Code-split charting libraries so you only load Recharts on dashboard pages
  • Server Components for non-interactive stat cards, resulting in zero client JS
  • React.memo on chart components with deep comparison selectors

Conclusion

Building a production analytics dashboard requires balancing data freshness, performance, and UX. By using React Query for server state, SSE for live data, and thoughtful component splitting between server and client, we can create a responsive, maintainable application.

Working on a data-heavy frontend? We'd love to hear about your project.

Building something great? Let's talk →