This tutorial builds a complete analytics dashboard: KPI cards, line charts, bar charts and pie charts, with time filter, simulated API data loading and responsive design — the kind of dashboard you see in enterprise.
npx create-react-app dashboard-analytics --template javascript
cd dashboard-analytics
npm install recharts date-fns
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
We start with a custom hook that manages data loading and time filtering.
Definition: Special React function (starting with "use") that enables using React features in functional components. Hooks include built-in hooks (useState, useEffect, useMemo, useCallback) and custom hooks you create.
Purpose: Encapsulate reusable logic (state, effects) in reusable functions rather than duplicating code.
Why here: Custom hooks like useMetrics allow extracting all data logic (loading, filtering, calculations) from the component. Other components can reuse this hook — it's horizontal logic reuse.
import { useState, useEffect, useCallback, useMemo } from 'react';
import { subDays, format, parseISO, isAfter } from 'date-fns';
// ── Generate simulated data (replace with real API call) ──
function generateData(days = 90) {
const data = [];
let revenue = 5000;
let users = 1200;
for (let i = days; i >= 0; i--) {
const date = subDays(new Date(), i);
# Realistic variation with upward trend
revenue *= (1 + (Math.random() - 0.45) * 0.08);
users += Math.floor((Math.random() - 0.3) * 30);
data.push({
date: format(date, 'yyyy-MM-dd'),
revenue: Math.max(0, Math.round(revenue)),
users: Math.max(0, users),
conversions: Math.round(users * (0.02 + Math.random() * 0.04)),
sessions: Math.round(users * (1.5 + Math.random() * 0.5)),
});
}
return data;
}
const TRAFFIC_SOURCES = [
{ name: 'Organic', value: 42, color: '#6366f1' },
{ name: 'Direct', value: 28, color: '#22d3ee' },
{ name: 'Social', value: 18, color: '#f59e0b' },
{ name: 'Email', value: 12, color: '#10b981' },
];
export default function useMetrics() {
const [allData] = useState(() => generateData(90));
const [range, setRange] = useState(30); # days
const [loading, setLoading] = useState(false);
# Simulate API loading with delay
const changeRange = useCallback((days) => {
setLoading(true);
setTimeout(() => { setRange(days); setLoading(false); }, 400);
}, []);
# Data filtered by selected time period
const filteredData = useMemo(() => {
const cutoff = subDays(new Date(), range);
return allData.filter(d => isAfter(parseISO(d.date), cutoff));
}, [allData, range]);
# KPIs calculated once when filteredData changes
const kpis = useMemo(() => {
if (!filteredData.length) return {};
const last = filteredData[filteredData.length - 1];
const first = filteredData[0];
const totalRevenue = filteredData.reduce((s, d) => s + d.revenue, 0);
const revGrowth = ((last.revenue - first.revenue) / first.revenue) * 100;
return {
totalRevenue,
revGrowth: revGrowth.toFixed(1),
totalUsers: last.users,
avgConvRate: (filteredData.reduce((s, d) => s + d.conversions / d.users, 0) / filteredData.length * 100).toFixed(2),
totalSessions: filteredData.reduce((s, d) => s + d.sessions, 0),
};
}, [filteredData]);
return { data: filteredData, kpis, range, changeRange, loading, trafficSources: TRAFFIC_SOURCES };
}
Definition: React hook that memoizes (caches) a computed value. If dependencies haven't changed, useMemo returns the cached value instead of recomputing.
Purpose: Avoid expensive recalculations on each render — essential for complex computations (filtering, aggregations) and performance.
Why here: filteredData and kpis are recalculated only when their dependencies (allData, range) change. Without useMemo, every render would recalculate these values, which is slow if you have 10,000 data points and render 100x per second.
Definition: React hook that memoizes a function. If dependencies don't change, useCallback returns the same function reference instead of creating a new one each render.
Purpose: Prevent function callbacks from triggering unnecessary re-renders of child components that receive this function as a prop.
Why here: changeRange is passed to filter buttons. Without useCallback, every parent render creates a new changeRange function, which forces children to re-render even if behavior hasn't changed.
export default function KPICard({ title, value, change, icon, color = 'indigo' }) {
const isPositive = parseFloat(change) >= 0;
return (
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-5 flex items-start gap-4">
<div className={`p-3 rounded-lg bg-${color}-50 text-${color}-600 text-2xl`}>
{icon}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm text-gray-500 font-medium truncate">{title}</p>
<p className="text-2xl font-bold text-gray-900 mt-0.5">{value}</p>
{change !== undefined && (
<p className={`text-sm mt-1 font-medium ${isPositive ? 'text-green-600' : 'text-red-500'}`}>
{isPositive ? '↑' : '↓'} {Math.abs(parseFloat(change))}% vs prev period
</p>
)}
</div>
</div>
);
}
import {
LineChart, Line, BarChart, Bar, PieChart, Pie, Cell,
XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer
} from 'recharts';
import useMetrics from './hooks/useMetrics';
import KPICard from './components/KPICard';
// ── Custom Tooltip for charts ──
function CustomTooltip({ active, payload, label }) {
if (!active || !payload?.length) return null;
return (
<div className="bg-gray-900 text-white p-3 rounded-lg shadow-xl text-sm">
<p className="font-semibold mb-2">{label}</p>
{payload.map((p, i) => (
<p key={i} style={{ color: p.color }}>
{p.name} : <strong>{p.value}</strong>
</p>
))}
</div>
);
}
Definition: Recharts component that renders charts responsive — they adapt to container size instead of having a fixed size. Crucial for mobile-friendly dashboards.
Purpose: Charts stay readable on all screens (mobile, tablet, desktop) without manually updating dimensions.
Why here: Without ResponsiveContainer, charts have fixed size and overflow on mobile. With ResponsiveContainer, they adapt flexibly.
# ── Line chart: daily revenue ──
<ResponsiveContainer width="100%" height={260}>
<LineChart data={data} margin={{ top: 5, right: 10, bottom: 5, left: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#f1f5f9" />
<XAxis dataKey="date" tickFormatter={formatDate} />
<YAxis tickFormatter={formatCurrency} />
<Tooltip content={<CustomTooltip />} />
<Line type="monotone" dataKey="revenue" name="Revenue" stroke="#6366f1" strokeWidth={2} dot={false} />
</LineChart>
</ResponsiveContainer>
# ── Pie chart: traffic sources ──
<ResponsiveContainer width="100%" height={200}>
<PieChart>
<Pie data={trafficSources} dataKey="value" cx="50%" cy="50%" outerRadius={80}
label={({ name, value }) => `${name} ${value}%`} labelLine={false}>
{trafficSources.map((entry, i) => <Cell key={i} fill={entry.color} />)}
</Pie>
<Tooltip />
</PieChart>
</ResponsiveContainer>
# ── Bars: weekly sessions & conversions ──
<ResponsiveContainer width="100%" height={220}>
<BarChart data={weeklyData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="label" />
<YAxis />
<Tooltip content={<CustomTooltip />} />
<Legend />
<Bar dataKey="sessions" name="Sessions" fill="#6366f1" radius={[4, 4, 0, 0]} />
<Bar dataKey="conversions" name="Conversions" fill="#10b981" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
Definition: A component in React is a JavaScript function that returns JSX (HTML-like syntax). Props are arguments passed to the component (top-down data), state (useState) is mutable component data.
Purpose: Decompose UI into small reusable and maintainable components.
Why here: KPICard is a reusable component — we use it 4 times with different props. Without components, we'd have duplicated code everywhere.
Definition: Rendering is React's process of taking a component and producing DOM (HTML elements) based on current state and props. When state changes, React re-renders the component to reflect changes.
Purpose: Convert a component's declarative description into concrete DOM.
Why here: Understanding rendering is key to optimizing performance — React re-renders by default when props/state change, which can be slow on large components. That's why we use useMemo and useCallback.
import Dashboard from './Dashboard';
import './index.css';
export default function App() {
return <Dashboard />;
}
npm start
# Opens http://localhost:3000