Ce tutoriel construit un tableau de bord analytique complet : KPI cards, graphiques en ligne, barres et camembert, avec filtre temporel, chargement de données simulé depuis une API et design responsive — le type de dashboard qu'on retrouve en entreprise.
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
On commence par le hook custom qui gère le chargement des données et le filtre temporel.
Définition : Fonction spéciale React (commençant par "use") qui permet d'utiliser des features React dans les composants fonctionnels. Les hooks incluent les hooks built-in (useState, useEffect, useMemo, useCallback) et les hooks custom que vous créez.
But : Encapsuler la logique réutilisable (état, effets) dans des fonctions réutilisables plutôt que de dupliquer le code.
Pourquoi ici : Les hooks custom comme useMetrics permettent d'extraire toute la logique de données (chargement, filtrage, calculs) du composant. D'autres composants peuvent réutiliser ce hook — c'est la réutilisation de logique horizontale.
import { useState, useEffect, useCallback, useMemo } from 'react';
import { subDays, format, parseISO, isAfter } from 'date-fns';
// ── Générer des données simulées (remplacer par un vrai appel API) ──
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);
// Variation réaliste avec tendance haussière
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: 'Organique', 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); # jours
const [loading, setLoading] = useState(false);
// Simuler un chargement API avec délai
const changeRange = useCallback((days) => {
setLoading(true);
setTimeout(() => { setRange(days); setLoading(false); }, 400);
}, []);
// Données filtrées selon la période sélectionnée
const filteredData = useMemo(() => {
const cutoff = subDays(new Date(), range);
return allData.filter(d => isAfter(parseISO(d.date), cutoff));
}, [allData, range]);
// KPIs calculés une seule fois quand filteredData change
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 };
}
Définition : Hook React qui mémoïse (cache) une valeur calculée. Si les dépendances n'ont pas changé, useMemo retourne la valeur en cache au lieu de la recalculer.
But : Éviter les recalculs coûteux à chaque render — essentiel pour les calculs complexes (filtrages, agrégations) et la performance.
Pourquoi ici : filteredData et kpis sont recalculés seulement quand leur dépendances (allData, range) changent. Sans useMemo, chaque render recalculerait ces valeurs, ce qui est lent si on a 10 000 data points et on renderise par exemple 100x par seconde.
Définition : Hook React qui mémoïse une fonction. Si les dépendances ne changent pas, useCallback retourne la même référence de fonction au lieu de créer une nouvelle chaque render.
But : Éviter que les fonctions callback ne triggent inutilement les re-renders des composants enfants qui reçoivent cette fonction en prop.
Pourquoi ici : changeRange est passée aux boutons de filtre. Sans useCallback, chaque render du parent crée une nouvelle fonction changeRange, ce qui force les enfants à re-render même si le comportement n'a pas changé.
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 période préc.
</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 pour les graphiques ──
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>
);
}
Définition : Composant Recharts qui rend les graphiques responsive — ils s'adaptent à la taille du container au lieu d'avoir une taille fixe. Crucial pour les dashboards mobile-friendly.
But : Les graphiques restent lisibles sur tous les écrans (mobile, tablet, desktop) sans avoir à mettre à jour les dimensions manuellement.
Pourquoi ici : Sans ResponsiveContainer, les graphiques ont une taille fixe et débordent sur mobile. Avec ResponsiveContainer, ils s'adaptent flexiblement.
// ── Graphique en ligne : revenus quotidiens ──
<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="Revenu" stroke="#6366f1" strokeWidth={2} dot={false} />
</LineChart>
</ResponsiveContainer>
// ── Camembert : sources de trafic ──
<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>
// ── Barres : sessions & conversions hebdomadaires ──
<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>
Définition : Un composant React est une fonction JavaScript qui retourne du JSX (HTML-like syntax). Props sont les arguments passés au composant (données de haut en bas), state (useState) est les données mutables du composant.
But : Décomposer l'UI en petits composants réutilisables et maintenables.
Pourquoi ici : KPICard est un composant réutilisable — on l'utilise 4 fois avec des props différentes. Sans composants, on aurait du code dupliqué partout.
Définition : Le rendu est le processus de React qui prend un composant et produit du DOM (éléments HTML) basé sur l'état et les props actuels. Quand l'état change, React re-rend le composant pour refléter les changements.
But : Convertir la description déclarative d'un composant en DOM concret.
Pourquoi ici : Comprendre le rendering est clé pour optimiser les performances — React re-rend par défaut quand props/state changent, ce qui peut être lent sur de gros composants. C'est pourquoi on utilise useMemo et useCallback.
import Dashboard from './Dashboard';
import './index.css';
export default function App() {
return <Dashboard />;
}
npm start
# Ouvre http://localhost:3000