Împărțirea în componente smart (container) și dumb (presentational, „UI”) e cea mai utilă regulă de arhitectură pentru componente. Răspunde la întrebarea: cine are voie să știe de unde vin datele? Răspunsul: cât mai puține componente.
| Smart / container | Dumb / presentational | |
|---|---|---|
| Rol | Orchestrare: ia date, tratează acțiuni | Afișare: arată date, emite intenții |
| Injectează | store-uri, servicii API, Router | nimic de business (doar UI: ElementRef, DestroyRef…) |
| Comunicare | store / servicii | doar input(), output(), model() |
| Stare | stare de business (sau o deleagă unui store) | doar stare locală de UI (hover, expandat) |
| Reutilizare | rar — legată de un caz de utilizare | des — oriunde ai aceleași date |
| Testare | cu mock-uri / store fals | trivială: dai input-uri, verifici output-uri |
| Unde stă | features/x/pages | features/x/ui sau shared/ui |
Exemplu complet: un catalog de produse
O pagină smart (CatalogPage) cu un store de feature (CatalogStore) și trei componente dumb: filtrele, cardul de produs și sumarul coșului. Datele vin de la backend-ul simulat prin httpResource.
// SMART (container): știe DE UNDE vin datele și CE se întâmplă la o acțiune.
@Component({
selector: 'app-catalog-page',
imports: [ProductFilters, ProductCard, CartSummary],
providers: [CatalogStore], // store-ul trăiește cât pagina
template: `
<app-product-filters [categories]="categories" [(query)]="store.query" [(category)]="store.category" />
@for (p of store.products.value(); track p.id) {
<app-product-card [product]="p" (addToCart)="store.add($event)" />
}
<app-cart-summary
[lines]="store.cart()" [count]="store.cartCount()" [total]="store.cartTotal()"
(remove)="store.remove($event)" />
`,
})
export class CatalogPage {
protected readonly store = inject(CatalogStore);
protected readonly categories: ProductCategory[] = ['laptop', 'phone', 'audio', 'accessory'];
}Unde stau fișierele
features/catalog/
├── catalog.routes.ts
├── pages/
│ └── catalog-page.ts ← SMART (rutată)
├── data-access/
│ ├── catalog-store.ts ← stare + date
│ └── catalog-api.ts
├── ui/ ← DUMB, specifice feature-ului
│ ├── product-card.ts
│ ├── product-filters.ts
│ └── cart-summary.ts
└── models/
└── product.ts ← Product + toProduct(dto)
shared/ui/ ← DUMB, generice (fără domeniu)
├── button.ts
├── rating.ts
└── data-table.tsAnti-pattern: componenta „dumb” care e de fapt smart
// ✗ O componentă „dumb” care de fapt e smart deghizată
@Component({ selector: 'app-product-card', /* ... */ })
export class ProductCard {
private readonly cart = inject(CartStore); // ← dependență de stare globală
private readonly http = inject(HttpClient); // ← face request-uri
private readonly router = inject(Router); // ← decide navigarea
readonly productId = input.required<number>();
readonly product = httpResource<Product>(() => `/api/products/${this.productId()}`);
add() {
this.cart.add(this.product.value()!);
this.router.navigate(['/cart']);
}
}
// Consecințe: nu o poți refolosi în altă pagină (ex. în „recomandări” sau în admin),
// nu o poți testa fără mock-uri pentru HttpClient, Router și CartStore,
// iar o listă de 50 de carduri face 50 de request-uri.Ce are voie o componentă dumb
// ✓ Stare locală de UI într-o componentă dumb — complet OK
export class Accordion {
readonly items = input.required<Item[]>();
protected readonly expanded = signal<number | null>(null); // doar despre afișare
}
// ✓ Injectări „de UI” într-o componentă dumb — OK
inject(ElementRef); inject(DestroyRef); inject(LOCALE_ID); inject(Dialog /* CDK */);Reguli practice
- O pagină rutată = un container smart. Ea leagă URL-ul, store-ul și UI-ul.
- Componentele dumb nu injectează store-uri sau servicii de date. Dacă simți nevoia, ridică logica un nivel mai sus.
- Output-urile exprimă intenții, nu implementări:
(addToCart), nu(callCartServiceAndNavigate). - Nu trebuie ca TOT să fie dumb. Un widget complex dintr-o pagină (ex. „comentarii”) poate fi un mini-container smart cu store-ul lui — e mai bine decât să pasezi 15 input-uri prin 4 niveluri.
- Prop drilling peste 2–3 niveluri e semnalul că ai nevoie de un container intermediar sau de un store de feature.
Starea pe care o ține componenta smart stă de obicei într-un store: State management cu signals. Cum vorbesc cele două între ele e la input() și output().
Când DA, când NU
- Afișează date primite (card, rând de tabel, avatar, badge).
- E un control (filtru, rating, toggle) — starea vine prin
model(). - Vrei s-o refolosești în altă pagină sau în
shared/ui. - Vrei s-o testezi ușor sau s-o arăți în Storybook.
- E o pagină rutată.
- E un widget autonom cu propriile date (chat, notificări) folosit „drop-in” în mai multe pagini.
- Transformarea în dumb ar cere 10+ input-uri/output-uri pasate prin mai multe niveluri.