This guide shows you how to build a production-ready SaaS metrics dashboard using Recharts 2.13, React 19, and a REST API data source. You will have a working dashboard with live MRR, churn, and active user charts in about two hours, depending on your existing API setup.

What You'll Build

  • A responsive SaaS metrics dashboard with MRR, churn rate, and active user trend charts
  • A reusable chart component system wired to a real REST API endpoint
  • Automatic data refresh every 60 seconds without a full page reload
  • Accessible chart tooltips and ARIA labels that pass WCAG 2.2 AA audit
  • A deployable build tested on Node 20+ and pnpm 9

Prerequisites

  • Node 20 or higher installed locally
  • A Next.js 15 project already scaffolded (or create one with npx create-next-app@latest)
  • A REST API endpoint that returns time-series metrics in JSON (sample data provided in Step 2)
  • Basic familiarity with React hooks and TypeScript
  • pnpm 9 installed globally (npm i -g pnpm)

Step 1: Install Recharts and Supporting Packages

Recharts 2.13 is the current stable release as of October 2026. It ships with full React 19 compatibility and tree-shaking support, which reduces bundle size by roughly 40% compared to importing the full library.

pnpm add recharts
pnpm add -D @types/recharts

Also install date-fns for formatting timestamps on the X axis.

pnpm add date-fns

Common pitfall: Do not install the legacy recharts@1.x branch. Some older tutorials reference the v1 API, which uses a different component signature for ResponsiveContainer. Always pin to recharts@^2.13.

Step 2: Define Your Metrics Data Shape

Consistency in your data shape saves debugging time later. Define a TypeScript interface that matches the API response before writing a single chart component.

// types/metrics.ts
export interface MetricPoint {
  date: string; // ISO 8601, e.g. "2026-09-01"
  mrr: number;  // Monthly Recurring Revenue in cents
  churn: number; // Percentage, e.g. 2.4
  activeUsers: number;
}

export interface MetricsResponse {
  data: MetricPoint[];
  generatedAt: string;
}

If you do not have a live API yet, create a mock file at lib/mockMetrics.ts.

// lib/mockMetrics.ts
import type { MetricPoint } from "@/types/metrics";

export const mockData: MetricPoint[] = [
  { date: "2026-04-01", mrr: 1240000, churn: 3.1, activeUsers: 412 },
  { date: "2026-05-01", mrr: 1380000, churn: 2.8, activeUsers: 459 },
  { date: "2026-06-01", mrr: 1510000, churn: 2.4, activeUsers: 503 },
  { date: "2026-07-01", mrr: 1670000, churn: 2.1, activeUsers: 548 },
  { date: "2026-08-01", mrr: 1820000, churn: 1.9, activeUsers: 591 },
  { date: "2026-09-01", mrr: 1990000, churn: 1.7, activeUsers: 634 },
];

Step 3: Build a Data Fetching Hook With Auto-Refresh

React 19 introduces native support for async components, but client-side polling still works best with a custom hook. This hook fetches fresh data every 60 seconds.

// hooks/useMetrics.ts
"use client";

import { useState, useEffect, useCallback } from "react";
import type { MetricPoint } from "@/types/metrics";
import { mockData } from "@/lib/mockMetrics";

export function useMetrics(apiUrl?: string) {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  const fetchData = useCallback(async () => {
    try {
      if (!apiUrl) {
        setData(mockData);
        setLoading(false);
        return;
      }
      const res = await fetch(apiUrl);
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      const json = await res.json();
      setData(json.data);
    } catch (err) {
      setError((err as Error).message);
    } finally {
      setLoading(false);
    }
  }, [apiUrl]);

  useEffect(() => {
    fetchData();
    const interval = setInterval(fetchData, 60_000);
    return () => clearInterval(interval);
  }, [fetchData]);

  return { data, loading, error };
}

Pro tip: Replace mockData with a call to your real API endpoint once it is ready. The rest of the dashboard requires no changes.

Step 4: Create a Reusable Stat Card Component

Before building charts, add a summary row with key numbers. This is the first thing stakeholders look at.

// components/StatCard.tsx
interface StatCardProps {
  label: string;
  value: string;
  trend?: string;
}

export function StatCard({ label, value, trend }: StatCardProps) {
  return (
    

{label}

{value}

{trend && (

{trend}

)}
); }

Step 5: Build the MRR Line Chart

Why does MRR need its own chart component?

MRR is your most-watched SaaS metric. Isolating it in its own component makes it easy to swap the chart type later, say from a line to an area chart, without touching the dashboard layout.

// components/charts/MrrChart.tsx
"use client";

import {
  LineChart,
  Line,
  XAxis,
  YAxis,
  CartesianGrid,
  Tooltip,
  ResponsiveContainer,
} from "recharts";
import { format, parseISO } from "date-fns";
import type { MetricPoint } from "@/types/metrics";

interface MrrChartProps {
  data: MetricPoint[];
}

export function MrrChart({ data }: MrrChartProps) {
  const formatted = data.map((d) => ({
    ...d,
    mrrDisplay: d.mrr / 100, // convert cents to dollars
    label: format(parseISO(d.date), "MMM yy"),
  }));

  return (
    

Monthly Recurring Revenue

`$${(v / 1000).toFixed(0)}k`} tick={{ fontSize: 12 }} /> [`$${value.toLocaleString()}`, "MRR"] } />
); }

What if the chart renders at zero height?

This happens when ResponsiveContainer is inside a flex parent with no explicit height. Wrap the chart in a div with a fixed height class like h-60 or set height={240} on ResponsiveContainer directly.

Step 6: Build the Churn Rate and Active Users Charts

Use a BarChart for churn rate and an AreaChart for active users. The visual contrast helps readers interpret different metric types at a glance.

// components/charts/ChurnChart.tsx
"use client";

import {
  BarChart, Bar, XAxis, YAxis,
  CartesianGrid, Tooltip, ResponsiveContainer,
} from "recharts";
import { format, parseISO } from "date-fns";
import type { MetricPoint } from "@/types/metrics";

export function ChurnChart({ data }: { data: MetricPoint[] }) {
  const formatted = data.map((d) => ({
    ...d,
    label: format(parseISO(d.date), "MMM yy"),
  }));

  return (
    

Churn Rate (%)

`${v}%`} tick={{ fontSize: 12 }} /> [`${v}%`, "Churn"]} />
); }

Build the active users chart the same way. Replace BarChart with AreaChart, add an Area component with dataKey="activeUsers", and set fill="#a5f3fc" with stroke="#0891b2".

Step 7: Assemble the Dashboard Page

// app/dashboard/page.tsx
"use client";

import { useMetrics } from "@/hooks/useMetrics";
import { StatCard } from "@/components/StatCard";
import { MrrChart } from "@/components/charts/MrrChart";
import { ChurnChart } from "@/components/charts/ChurnChart";

export default function DashboardPage() {
  const { data, loading, error } = useMetrics(
    process.env.NEXT_PUBLIC_METRICS_API_URL
  );

  if (loading) return 

Loading metrics...

; if (error) return

Error: {error}

; const latest = data[data.length - 1]; return (

Product Metrics

); }

Step 8: Add Accessibility and ARIA Labels

WCAG 2.2 AA requires charts to communicate their data to screen reader users. Recharts does not add ARIA roles by default. Add a visually hidden data table beneath each chart.

// Inside MrrChart, after ResponsiveContainer

    {formatted.map((row) => (
      
    ))}
  
MRR monthly data
MonthMRR
{row.label} ${row.mrrDisplay.toLocaleString()}

The sr-only Tailwind class hides the table visually while keeping it accessible. This pattern is recommended in the WAI-ARIA Authoring Practices Guide.

Step 9: Test and Deploy

Run a production build locally before deploying.

pnpm build
pnpm start

Check the dashboard at http://localhost:3000/dashboard. Open DevTools and throttle the network to Slow 3G. The page should remain interactive because the charts only re-render on data change, not on every poll tick.

Deploy to Vercel with a single command.

vercel --prod

Set NEXT_PUBLIC_METRICS_API_URL in your Vercel environment variables before deploying to production.

Common pitfall: Environment variables prefixed with NEXT_PUBLIC_ are embedded at build time. If you change the API URL after deployment, you must redeploy, not just restart the server.

Frequently Asked Questions

Does Recharts work with React Server Components in Next.js 15?

No. Recharts relies on browser APIs and must run in a client component. Always add the "use client" directive at the top of any file that imports Recharts components. You can still fetch data in a Server Component and pass it down as props.

How do I handle an empty dataset without breaking the charts?

Add a guard before the chart renders. Check if (!data || data.length === 0) and return a placeholder element. Recharts will render a blank SVG canvas if passed an empty array, which can confuse users.

Can I use this dashboard with a GraphQL API instead of REST?

Yes. Replace the fetch call inside useMetrics with your preferred GraphQL client such as urql or @apollo/client. The hook interface and chart components do not need to change as long as the returned data matches the MetricPoint shape.

How is Recharts different from Victory or Nivo?

Recharts is built on standard SVG and has the smallest bundle footprint of the three at roughly 160 kB minified. Victory uses a similar approach but has a steeper API surface. Nivo produces more polished charts out of the box but ships closer to 400 kB. For SaaS dashboards where bundle size matters, Recharts is the practical default in 2026.

Why is my chart tooltip not showing on mobile?

Touch events on SVG elements behave differently across browsers. Set allowEscapeViewBox={{ x: false, y: false }} on the Tooltip component to prevent it from rendering outside the chart boundary on small screens.

Next Steps

Now that your dashboard is live, consider adding a date range picker using react-day-picker so users can filter metrics by custom periods. You can also wire in Supabase Realtime to push new data points without polling.

If your SaaS product tracks more complex funnels or cohort retention, the same component system scales to support stacked bar charts, scatter plots, and radial charts, all available natively in Recharts 2.13.

If you want a more opinionated dashboard architecture or need help connecting your metrics pipeline to a custom data warehouse, the team at Lenka Studio builds data-connected product interfaces for SaaS teams across Australia, Singapore, Canada, and the US. Get in touch to talk through your setup.