All Tutorials
react
typescript
recharts
dataviz
frontend
vite

Build a Data Dashboard with React, TypeScript, and Recharts

A code-along tutorial: build a single-page sales dashboard with React, TypeScript, Vite, and Recharts. A KPI stat row, a revenue line chart, and a category bar chart, backed by a typed mock dataset. No backend, no API key. Every file is here to paste and run.

13 min read2026-08-16

The short answer

Scaffold a React + TypeScript app with Vite, install Recharts, and feed it a typed mock dataset. The dashboard is a handful of small typed components: a StatCard for each KPI, a RevenueChart that wraps Recharts' LineChart in a ResponsiveContainer, and a CategoryChart that does the same with a BarChart. Recharts gives you charts as ordinary React components with first-class TypeScript types, so there is no canvas API to drive by hand. There is no server, no database, and no API key: the data is a static array you can later swap for a real fetch() call. Setup takes about ten minutes and every file is on this page.

  • Setup time: about 10 minutes
  • Stack: React 18, TypeScript, Vite, Recharts
  • Runtime dependencies: three (react-dom and recharts); everything else is dev tooling
  • No backend, no database, no API key, no signup
  • Deployable free on any static host (Vercel, Netlify, GitHub Pages)

What you'll build

By the end of this tutorial you'll have a single-page sales dashboard running in your browser: a row of KPI stat cards (total revenue, orders, average order value, and month-over-month growth), a line chart of revenue over time, and a bar chart that breaks revenue down by category. All of it is driven by a typed mock dataset, so there is no server, no database, and no API key to set up.

Every file you need is on this page. Paste each one into a fresh Vite project, follow the setup steps, and it runs. There is no repo to clone and no hidden files. The tutorial also explains the decisions behind the code, so you can change the data, the charts, and the layout without guessing.

Why Recharts (and one honest trade-off)

Recharts is the most widely-used charting library in the React ecosystem, and the reason is that it fits how React already works. A chart is a tree of components: <LineChart> holds a <Line>, an <XAxis>, a <YAxis>, a <Tooltip>. You pass data in as a prop and describe what to draw with JSX, the same way you build the rest of your UI. There is no imperative canvas API to poke at on every update, which is the part that makes hand-rolled charting with a lower-level tool tedious. Recharts is built on D3 under the hood, so the math is battle-tested, but you never touch D3 directly. And it ships real TypeScript types, so your data shape and the chart's dataKey props are checked at compile time.

Here is the trade-off, up front and honest: Recharts renders SVG. One DOM node per point. At dashboard scale (a few dozen to a few hundred points) that is exactly right and gives you crisp, styleable, accessible charts for free. It is the wrong tool for tens of thousands of points or a real-time stream redrawing many times a second; there you want a canvas or WebGL library instead. For the KPIs-and-trends dashboards most people actually build, SVG is the correct default, and Recharts is the fastest way to a clean one.

Setup

Scaffold a fresh React + TypeScript project with Vite, then add the one runtime library this dashboard needs, recharts:

bash
npm create vite@latest dashboard -- --template react-ts
cd dashboard
npm install recharts
npm run dev

npm create vite scaffolds the project, including react, react-dom, typescript, vite, and @vitejs/plugin-react in package.json. The only package you add by hand is recharts, which ships its own TypeScript types, so there is no separate @types/recharts to install. Vite's template already gives you a working package.json; the scripts block looks like this:

json
{
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "preview": "vite preview"
  }
}

npm run dev starts the dev server (usually at http://localhost:5173) with hot reload. npm run build type-checks with tsc and produces a static bundle in dist/ you can deploy anywhere. Every file below goes in the src/ directory Vite created. The template ships a src/main.tsx that already renders <App />, so you don't touch it: you are replacing App.tsx and adding a few new files next to it.

Define the data types

Start with the shape of the data, so the compiler catches a typo in a dataKey the moment you make one. A dashboard like this has two kinds of records: one point per month for the revenue trend, and one row per category for the breakdown. Create src/types.ts:

typescript
export interface SalesDataPoint {
  month: string;
  revenue: number;
  orders: number;
}

export interface CategorySales {
  category: string;
  revenue: number;
}

SalesDataPoint is one month: a label, the revenue for that month, and the order count. CategorySales is one slice of the breakdown: a category name and its revenue. These two interfaces are the contract every chart and the KPI math below depend on. When you later replace the mock data with a real API response, these are the types you map that response into, and nothing downstream has to change.

The mock data module

Real dashboards read their numbers from an API. To keep this tutorial focused on the rendering layer, we stand in a static, typed dataset instead: eight months of sales and a four-way category split. Create src/data.ts:

typescript
import type { CategorySales, SalesDataPoint } from "./types";

export const monthlySales: SalesDataPoint[] = [
  { month: "Jan", revenue: 42000, orders: 320 },
  { month: "Feb", revenue: 45500, orders: 351 },
  { month: "Mar", revenue: 39800, orders: 298 },
  { month: "Apr", revenue: 51200, orders: 402 },
  { month: "May", revenue: 58700, orders: 447 },
  { month: "Jun", revenue: 61300, orders: 468 },
  { month: "Jul", revenue: 68900, orders: 521 },
  { month: "Aug", revenue: 72400, orders: 549 },
];

export const categorySales: CategorySales[] = [
  { category: "Subscriptions", revenue: 184000 },
  { category: "One-time", revenue: 96500 },
  { category: "Add-ons", revenue: 58200 },
  { category: "Services", revenue: 41100 },
];

Because both arrays are typed, TypeScript will reject a row that is missing a field or has a string where a number belongs. That is the whole point of defining the types first: the data file is checked against them. When you are ready for real data, this is the one module you replace. Swap the two export const arrays for an async fetch() that returns the same shapes, load them in App with useState plus useEffect, and every component below keeps working unchanged. Our REST API tutorial builds exactly the kind of endpoint you'd fetch from.

The StatCard component

The KPI row is four copies of one small, presentational component. It takes a label, a value, and an optional hint, and holds no state of its own. Create src/StatCard.tsx:

tsx
interface StatCardProps {
  label: string;
  value: string;
  hint?: string;
}

export function StatCard({ label, value, hint }: StatCardProps) {
  return (
    <div className="stat-card">
      <span className="stat-label">{label}</span>
      <span className="stat-value">{value}</span>
      {hint && <span className="stat-hint">{hint}</span>}
    </div>
  );
}

value is a string, not a number, on purpose: the card renders whatever it is handed, and the formatting (dollar signs, percentages, thousands separators) happens once in App where the numbers live. That keeps the card dumb and reusable. The hint && ... line is the standard React pattern for rendering something only when a value is present, so a card without a hint just leaves that row out.

The revenue line chart

Now the first chart. RevenueChart takes the monthly array and draws a line of revenue over time. Every part of it is a Recharts component. Create src/RevenueChart.tsx:

tsx
import {
  ResponsiveContainer,
  LineChart,
  Line,
  XAxis,
  YAxis,
  CartesianGrid,
  Tooltip,
} from "recharts";
import type { SalesDataPoint } from "./types";

interface RevenueChartProps {
  data: SalesDataPoint[];
}

export function RevenueChart({ data }: RevenueChartProps) {
  return (
    <ResponsiveContainer width="100%" height={280}>
      <LineChart data={data} margin={{ top: 8, right: 16, bottom: 8, left: 8 }}>
        <CartesianGrid strokeDasharray="3 3" stroke="#eee" />
        <XAxis dataKey="month" />
        <YAxis tickFormatter={(value) => `$${value / 1000}k`} />
        <Tooltip formatter={(value) => `$${Number(value).toLocaleString()}`} />
        <Line
          type="monotone"
          dataKey="revenue"
          stroke="#2563eb"
          strokeWidth={2}
          dot={{ r: 3 }}
        />
      </LineChart>
    </ResponsiveContainer>
  );
}

Read it top to bottom and the model is clear. <ResponsiveContainer> measures its parent and gives the chart a real width and height, which is what makes the chart resize with the page instead of needing a fixed pixel size. <LineChart data={data}> hands the array to the whole subtree. <XAxis dataKey="month"> tells Recharts which field of each row is the x-axis label, and <Line dataKey="revenue"> picks the field to plot. The dataKey strings are the one place a typo would bite, which is why the typed data pays off: a dataKey that doesn't exist on SalesDataPoint renders an empty line and you'll notice at once. tickFormatter and the Tooltip formatter turn raw numbers into readable dollars, and type="monotone" is what gives the line its smooth curve instead of straight segments.

The category bar chart

The second chart is the same idea with a different shape: a <BarChart> for the category breakdown. Once you have written one Recharts chart, the rest follow the same pattern. Create src/CategoryChart.tsx:

tsx
import {
  ResponsiveContainer,
  BarChart,
  Bar,
  XAxis,
  YAxis,
  CartesianGrid,
  Tooltip,
} from "recharts";
import type { CategorySales } from "./types";

interface CategoryChartProps {
  data: CategorySales[];
}

export function CategoryChart({ data }: CategoryChartProps) {
  return (
    <ResponsiveContainer width="100%" height={280}>
      <BarChart data={data} margin={{ top: 8, right: 16, bottom: 8, left: 8 }}>
        <CartesianGrid strokeDasharray="3 3" stroke="#eee" />
        <XAxis dataKey="category" />
        <YAxis tickFormatter={(value) => `$${value / 1000}k`} />
        <Tooltip formatter={(value) => `$${Number(value).toLocaleString()}`} />
        <Bar dataKey="revenue" fill="#2563eb" radius={[4, 4, 0, 0]} />
      </BarChart>
    </ResponsiveContainer>
  );
}

The structure is identical to the line chart: a responsive container, the same grid, axes, and tooltip, and one <Bar> instead of a <Line>. The dataKey="category" on the x-axis and dataKey="revenue" on the bar point at the two fields of CategorySales. The radius={[4, 4, 0, 0]} rounds the top corners of each bar, a small touch that makes the chart look finished. If you'd rather show the split as a pie, Recharts has <PieChart> and <Pie>; the same data array works, you just map each slice's value from dataKey="revenue".

Shore up your React first

If the component and props patterns here felt shaky, the fastest free way to solidify React is Scrimba's interactive Learn React course, where you edit components inside the lesson. For the ranked rundown of every free option, see our best free React courses guide, and keep the React cheat sheet open while you build.

Wire it up in App.tsx

App is the one place the numbers get turned into KPIs and the layout gets assembled. It computes the four stats from the monthly data, then arranges the stat row and the two charts in a grid. Replace the generated src/App.tsx with this:

tsx
import { StatCard } from "./StatCard";
import { RevenueChart } from "./RevenueChart";
import { CategoryChart } from "./CategoryChart";
import { monthlySales, categorySales } from "./data";
import "./App.css";

function formatCurrency(value: number): string {
  return `$${value.toLocaleString()}`;
}

export default function App() {
  const totalRevenue = monthlySales.reduce((sum, m) => sum + m.revenue, 0);
  const totalOrders = monthlySales.reduce((sum, m) => sum + m.orders, 0);
  const avgOrderValue = Math.round(totalRevenue / totalOrders);

  const latest = monthlySales[monthlySales.length - 1];
  const previous = monthlySales[monthlySales.length - 2];
  const growth = ((latest.revenue - previous.revenue) / previous.revenue) * 100;

  return (
    <div className="dashboard">
      <header className="dashboard-header">
        <h1>Sales dashboard</h1>
        <p>Year-to-date performance, from a typed mock dataset.</p>
      </header>

      <section className="stat-row">
        <StatCard label="Total revenue" value={formatCurrency(totalRevenue)} />
        <StatCard label="Orders" value={totalOrders.toLocaleString()} />
        <StatCard label="Avg order value" value={formatCurrency(avgOrderValue)} />
        <StatCard
          label="MoM growth"
          value={`${growth.toFixed(1)}%`}
          hint={`${latest.month} vs ${previous.month}`}
        />
      </section>

      <section className="chart-grid">
        <div className="chart-card">
          <h2>Revenue over time</h2>
          <RevenueChart data={monthlySales} />
        </div>
        <div className="chart-card">
          <h2>Revenue by category</h2>
          <CategoryChart data={categorySales} />
        </div>
      </section>
    </div>
  );
}

The KPIs are plain array math, not a library: reduce sums revenue and orders, average order value is one divided by the other, and month-over-month growth compares the last two months. Doing the math here, in one place, is why StatCard could stay a dumb string renderer. Notice the charts get the raw arrays as props (data={monthlySales}), while the cards get formatted strings; that split, compute-and-format at the top, render below, is the pattern that keeps a growing dashboard from turning into spaghetti.

The import "./App.css" line pulls in the layout. Vite's template ships an App.css you can replace with this grid, which puts the KPIs in a responsive row and the two charts side by side (they stack on narrow screens). Create or overwrite src/App.css:

css
.dashboard {
  max-width: 1000px;
  margin: 0 auto;
  padding: 24px;
  font-family: system-ui, sans-serif;
  color: #1f2937;
}

.dashboard-header h1 {
  margin: 0 0 4px;
}

.dashboard-header p {
  margin: 0 0 24px;
  color: #6b7280;
}

.stat-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 16px;
  margin-bottom: 24px;
}

.stat-card {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 16px;
  border: 1px solid #e5e7eb;
  border-radius: 10px;
  background: #fff;
}

.stat-label {
  font-size: 13px;
  color: #6b7280;
}

.stat-value {
  font-size: 24px;
  font-weight: 600;
}

.stat-hint {
  font-size: 12px;
  color: #9ca3af;
}

.chart-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 16px;
}

.chart-card {
  padding: 16px;
  border: 1px solid #e5e7eb;
  border-radius: 10px;
  background: #fff;
}

.chart-card h2 {
  margin: 0 0 12px;
  font-size: 15px;
  font-weight: 600;
}

Run it

Start the dev server if it is not already running:

bash
npm run dev

Open the local URL Vite prints (usually http://localhost:5173) and check each piece:

  • The KPI row shows four cards: total revenue, orders, average order value, and month-over-month growth with a Aug vs Jul hint.
  • The revenue chart draws a smooth line trending up across the eight months, with a $k axis and a dollar-formatted tooltip on hover.
  • The category chart draws four bars, tallest for Subscriptions, with the same dollar tooltip.
  • Resize the browser window. Both charts resize to fit, and the cards and charts reflow, because ResponsiveContainer and the CSS grid are doing their jobs.
  • Change a number in src/data.ts and save. Hot reload updates the KPIs and both charts instantly.

If a chart renders blank, the usual cause is a dataKey that doesn't match a field name in your data, or a ResponsiveContainer whose parent has no height; give the .chart-card real size (the CSS above does) and double-check the dataKey strings against types.ts.

Where to go next

Be clear about what this tutorial did and didn't teach. It taught the rendering layer: typed data in, charts and KPIs out, laid out responsively. It deliberately did not cover data fetching or real-time updates, because the dataset is a static array. That is the honest limitation, and it is the right scope for learning Recharts without a backend in the way. When you want real numbers, the change is contained: replace src/data.ts with a fetch() that returns the same SalesDataPoint[] and CategorySales[] shapes, hold the results in useState, and load them in a useEffect. The charts don't change at all.

Three honest next steps build on this without a rewrite. Add a real API. Our REST API with Express and TypeScript tutorial builds an endpoint you can fetch the dashboard's data from, which turns this from a static demo into a real, data-backed page. Add a third chart or a filter. A date-range selector that slices monthlySales before passing it in is a change in App alone, because the charts are already driven entirely by props. Compare it to the analysis side. Our build a data dashboard for free path teaches the Python and pandas side, computing the metrics; this tutorial is the React rendering half of the same job.

For the full sequenced route into frontend work, see /learn/frontend.

Frequently asked questions

Do I need a backend to build a dashboard?

No. This entire dashboard runs in the browser with no server, no database, and no API key. The data is a typed array in src/data.ts. You only need a backend when the numbers should come from a live source, and even then the React code barely changes: you swap the static arrays for a fetch() call that returns the same shapes and load the result with useState and useEffect. The charts and KPI cards stay exactly as they are.

Recharts vs Chart.js vs D3 for a free project?

For a React dashboard, Recharts is usually the best fit: charts are ordinary React components with TypeScript types, so they slot into your app the way the rest of your UI does. Chart.js is excellent and canvas-based, which scales to more data points, but you drive it through an imperative API or a wrapper rather than plain JSX. D3 is the most powerful and the lowest level; you get total control at the cost of writing a lot more code. All three are free and open source. Reach for Recharts first for a standard KPIs-and-trends dashboard, Chart.js when you have far more data, and D3 when you need a custom visualization the others can't express.

How do I connect this to real data later?

Replace src/data.ts with a fetch. In App, add const [sales, setSales] = useState<SalesDataPoint[]>([]) and a useEffect that fetches your API, maps the response into the SalesDataPoint and CategorySales shapes, and calls the setters. Pass those state arrays to the charts instead of the imported constants. Because the components are typed against those interfaces and driven by props, nothing inside RevenueChart or CategoryChart needs to change. Our REST API tutorial builds an endpoint that returns exactly this kind of JSON.

Can I deploy this dashboard for free?

Yes. Run npm run build to produce a static bundle in dist/, then drop it on any static host. Vercel, Netlify, and GitHub Pages all have free tiers that serve a Vite build with no configuration beyond pointing them at your repo. Because there is no backend, there is nothing to pay for or keep running: it is just static HTML, CSS, and JavaScript.

Does Recharts work well with TypeScript?

Yes. Recharts ships its own type definitions, so you don't install a separate @types package. Your data arrays are typed by your own interfaces, and the compiler checks the props you pass to each chart. The one thing types can't catch is a dataKey string that doesn't match a field, since it is just a string; keep the dataKey values lined up with your interface field names and a mismatch shows as an empty series you'll spot immediately.

Keep going on FreeCodingCourses