« Dois-je le déplacer dans shared ? » est probablement la question d’architecture la plus fréquente dans un projet Angular. La réponse courte : moins souvent que vous ne le pensez. shared est réservé au code générique (sans domaine métier) et stable. Tout ce que vous y mettez devient une dépendance de plusieurs features — et toute modification peut toutes les casser.
Ne déplacez dans shared que ce qui est déjà utilisé dans plusieurs features, sans termes métier et sans dépendances de données. Sinon, le composant reste à côté du code qui l’utilise.
Pourquoi: Un seul usage ne justifie pas une abstraction. Vous ne savez pas encore quelles variations vont apparaître.
Étape suivante: Laissez-le là. Réévaluez quand le deuxième (ou troisième) usage réel apparaît.
Checklist avant de déplacer
Avant de déplacer quelque chose dans shared, répondez OUI à toutes ces questions :
[ ] Il est utilisé MAINTENANT par au moins 2 features différentes (pas « il pourrait l’être »)
[ ] Son nom et son API ne contiennent aucun terme métier
[ ] Il n’injecte ni stores, ni services de données, ni le Router
[ ] Il communique uniquement via input / output / model / ng-content
[ ] Tous les usages ont le même comportement (pas de flags par usage)
[ ] Vous acceptez de traiter son API comme une API publique (toute modification touche tout le monde)Comment extraire correctement : le générique dans shared, le métier dans la feature
Vous déplacez rarement un composant « tel quel ». En général, vous le découpez : la partie générique (apparence, comportement) va dans shared/ui, et la partie métier (libellés, couleurs de statut, règles) reste dans la feature sous la forme d’un wrapper fin.
// shared/ui/badge.ts — générique, sans domaine métier
@Component({
selector: 'app-badge',
host: { '[class]': '"badge " + tone()' },
template: `<ng-content />`,
})
export class Badge {
readonly tone = input<'neutral' | 'success' | 'danger' | 'warning'>('neutral');
}
// features/orders/ui/order-status-badge.ts — wrapper métier, reste dans la feature
@Component({
selector: 'app-order-status-badge',
imports: [Badge],
template: `<app-badge [tone]="tone()">{{ label() }}</app-badge>`,
})
export class OrderStatusBadge {
readonly status = input.required<OrderStatus>();
protected readonly label = computed(() => ORDER_LABELS[this.status()]);
protected readonly tone = computed(() => (this.status() === 'cancelled' ? 'danger' : 'success'));
}// features/orders/ui/order-status-badge.ts — utilisé dans orders ET dans le dashboard
@Component({
selector: 'app-order-status-badge',
template: `<span [class]="'badge ' + color()">{{ label() }}</span>`,
})
export class OrderStatusBadge {
readonly status = input.required<OrderStatus>();
protected readonly label = computed(() => ORDER_LABELS[this.status()]);
protected readonly color = computed(() => (this.status() === 'cancelled' ? 'red' : 'green'));
}Composants métier utilisés par plusieurs features
UserAvatar est utilisé dans orders, comments et admin. Il n’est pas générique (il concerne les utilisateurs), mais il est partagé. La solution : il reste dans la feature propriétaire (users) et est exposé via une API publique. Ainsi, on sait qui le maintient, et le reste de la feature users reste privé.
// features/users/index.ts — la « vitrine » de la feature users pour le reste de l’application
export { UserAvatar } from './ui/user-avatar';
export type { User } from './models/user';
// tout le reste (le store interne, les pages, les mappers) reste privé
// features/orders/ui/order-row.ts
import { UserAvatar } from '@features/users'; // ✓ via l’API publique
// import { UserAvatar } from '@features/users/ui/user-avatar'; ✗ à l’intérieur d’une autre featureSignes qu’un composant shared doit être ramené dans la feature
// Le signe qu’un composant shared doit être découpé (ou ramené dans la feature)
@Component({ selector: 'app-card' })
export class Card {
readonly compact = input(false);
readonly showPrice = input(true);
readonly isCheckout = input(false); // ← une feature « fuit » dans shared
readonly hideAvatarOnMobile = input(false);
readonly legacyMode = input(false); // ← plus personne ne sait pourquoi il existe
}- Des inputs du genre « je suis utilisé sur la page X ».
- Chaque modification demandée par une feature oblige à vérifier toutes les autres.
- Une seule feature utilise encore le composant.
L’emplacement de chaque dossier : Structure des dossiers ; les règles sur qui peut importer quoi : Barrels, alias et frontières.
Quand OUI, quand NON
- Boutons, cartes, badges, dialogues, tableaux génériques, paginateurs.
- Directives et pipes d’UI : tooltip, autofocus, timeAgo.
- Layouts : en-tête de page, état vide, skeleton.
- Tout ce qui porte un nom métier :
ProductCard,InvoiceTable,UserFilters. - Les composants utilisés dans une seule feature (même à 10 endroits).
- Les composants qui injectent des stores ou des API.
Deux composants presque identiques dans deux features qui évoluent indépendamment sont plus faciles à maintenir qu’un composant commun plein de if. Attendez de voir clairement ce qui est commun (la « règle de trois »).