Presenter / ViewModel
Séparer la logique de présentation du rendu
Problème
Les composants React finissent souvent avec deux responsabilités mélangées : transformer les données brutes pour l'affichage, et rendre le JSX. Quand la logique de formatage grossit, le composant devient difficile à tester et à maintenir.
Presenter
Le Presenter prend les données brutes et produit des données prêtes à l'affichage. Le composant ne fait que du rendu.
// Données brutes venant du store ou de l'API
interface Order {
id: string;
createdAt: string; // ISO 8601
totalCents: number;
status: 'pending' | 'shipped' | 'delivered' | 'cancelled';
items: OrderItem[];
}
// Données formatées pour l'affichage
interface OrderViewModel {
id: string;
date: string; // "12 mai 2026"
total: string; // "42,00 €"
statusLabel: string; // "En cours de livraison"
statusColor: string; // "text-yellow-600"
itemCount: string; // "3 articles"
}
function presentOrder(order: Order): OrderViewModel {
const STATUS_MAP = {
pending: { label: 'En attente', color: 'text-gray-500' },
shipped: { label: 'En cours de livraison', color: 'text-yellow-600' },
delivered: { label: 'Livré', color: 'text-green-600' },
cancelled: { label: 'Annulé', color: 'text-red-500' },
};
return {
id: order.id,
date: new Intl.DateTimeFormat('fr-FR', { dateStyle: 'long' }).format(new Date(order.createdAt)),
total: new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(order.totalCents / 100),
statusLabel: STATUS_MAP[order.status].label,
statusColor: STATUS_MAP[order.status].color,
itemCount: `${order.items.length} article${order.items.length > 1 ? 's' : ''}`,
};
}
// Le composant ne contient plus aucune logique de formatage
function OrderCard({ order }: { order: Order }) {
const vm = presentOrder(order);
return (
<div>
<p>{vm.date} · {vm.itemCount}</p>
<p className={vm.statusColor}>{vm.statusLabel}</p>
<p>{vm.total}</p>
</div>
);
}Presenter avec hook
En React, il est courant d'encapsuler le presenter dans un hook :
function useOrderViewModel(order: Order): OrderViewModel {
return useMemo(() => presentOrder(order), [order]);
}
function OrderCard({ order }: { order: Order }) {
const vm = useOrderViewModel(order);
return (/* JSX */)
}Le useMemo évite de recalculer le ViewModel à chaque rendu si order n'a pas changé.
Testabilité
Le vrai avantage du Presenter : la logique de formatage est une fonction pure, testable sans React.
describe('presentOrder', () => {
it('formate le total en euros', () => {
const vm = presentOrder({ ...baseOrder, totalCents: 4200 });
expect(vm.total).toBe('42,00 €');
});
it('affiche le bon label pour shipped', () => {
const vm = presentOrder({ ...baseOrder, status: 'shipped' });
expect(vm.statusLabel).toBe('En cours de livraison');
expect(vm.statusColor).toBe('text-yellow-600');
});
});Tester la même logique dans un composant nécessiterait un render, des queries DOM, et un environnement jsdom.