Angular is an opinionated framework — it makes many decisions for you. That's a strength in large teams, but it also means there's a clear gap between "code that compiles" and "code that scales." The patterns covered here separate Angular codebases that hold up over three years from ones that become unmaintainable after the first major feature.Angular é um framework opinativo — ele toma muitas decisões por você. Isso é uma força em times grandes, mas também significa que há uma lacuna clara entre "código que compila" e "código que escala". Os padrões abordados aqui separam codebases Angular que resistem por três anos daqueles que se tornam impossíveis de manter após a primeira feature importante.
1. Component Architecture: Smart vs. Dumb1. Arquitetura de Componentes: Smart vs. Dumb
The most impactful pattern in Angular component design is the split between smart (container) components and dumb (presentational) components. Smart components know about services, state, and routing. Dumb components receive data via @Input(), emit events via @Output(), and know nothing about the world outside them.O padrão mais impactante no design de componentes Angular é a divisão entre componentes smart (container) e componentes dumb (apresentação). Componentes smart conhecem serviços, estado e roteamento. Componentes dumb recebem dados via @Input(), emitem eventos via @Output() e não sabem nada sobre o mundo fora deles.
// ✗ Dumb component doing smart things — untestable in isolation// ✗ Componente dumb fazendo coisas smart — não testável em isolamento
@Component({ selector: 'app-user-card' })
export class UserCardComponent {
user$ = inject(UserService).getUser(this.route.snapshot.params['id']);
onDelete() { inject(UserService).delete(this.userId).subscribe(); }
}
// ✓ Smart container handles data; dumb component handles display// ✓ Container smart lida com dados; componente dumb lida com exibição
@Component({ selector: 'app-user-page' }) // smart// smart
export class UserPageComponent {
user$ = inject(UserService).getUser(this.route.snapshot.params['id']);
onDelete(id: string) { inject(UserService).delete(id).subscribe(); }
}
@Component({ selector: 'app-user-card' }) // dumb// dumb
export class UserCardComponent {
@Input({ required: true }) user!: User;
@Output() delete = new EventEmitter<string>();
}Dumb components are trivial to test (just pass inputs, assert outputs), trivial to reuse, and trivial to move. Aim for 80% dumb components in any non-trivial Angular app.Componentes dumb são triviais de testar (apenas passe inputs, verifique outputs), triviais de reutilizar e triviais de mover. Busque 80% de componentes dumb em qualquer app Angular não trivial.
2. OnPush Change Detection: Default for Everything2. OnPush Change Detection: Padrão para Tudo
Angular's default change detection checks every component on every event. ChangeDetectionStrategy.OnPush restricts checks to when an @Input() reference changes, an async pipe resolves, or markForCheck() is called explicitly. On a page with 50+ components, this is the difference between smooth and jank.A detecção de mudanças padrão do Angular verifica cada componente em cada evento. ChangeDetectionStrategy.OnPush restringe as verificações para quando uma referência de @Input() muda, um async pipe resolve ou markForCheck() é chamado explicitamente. Em uma página com 50+ componentes, esta é a diferença entre suave e travado.
// Set this on every component — make it your team's default// Defina isso em todos os componentes — torne-o o padrão do seu time
@Component({
selector: 'app-user-card',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<div>{{ user.name }}</div>`
})
export class UserCardComponent {
@Input({ required: true }) user!: User;
}
// With OnPush, mutations to the same object won't trigger re-render// Com OnPush, mutações no mesmo objeto não acionarão re-renderização
// ✗ Won't update the view// ✗ Não vai atualizar a view
this.user.name = 'New Name';
// ✓ New reference — triggers re-render// ✓ Nova referência — aciona re-renderização
this.user = { ...this.user, name: 'New Name' };Enforce it with a linter rule: Add @angular-eslint/prefer-on-push-component-change-detection to your ESLint config. The rule flags any component missing OnPush, making it a team standard rather than a personal discipline.Aplique com uma regra de linter: Adicione @angular-eslint/prefer-on-push-component-change-detection à sua config ESLint. A regra sinaliza qualquer componente sem OnPush, tornando-o um padrão do time em vez de disciplina pessoal.
3. RxJS: The Five Pitfalls That Kill Angular Apps3. RxJS: Os Cinco Armadilhas que Matam Apps Angular
Memory leaks from unmanaged subscriptionsMemory leaks por assinaturas não gerenciadas
// ✗ Subscription never cleaned up — leaks on component destroy// ✗ Assinatura nunca limpa — vaza ao destruir o componente
ngOnInit() {
this.userService.users$.subscribe(users => this.users = users);
}
// ✓ Option 1: async pipe (Angular manages lifecycle automatically)// ✓ Opção 1: async pipe (Angular gerencia o ciclo de vida automaticamente)
users$ = this.userService.users$;
// template: {{ users$ | async }}// template: {{ users$ | async }}
// ✓ Option 2: takeUntilDestroyed (Angular 16+)// ✓ Opção 2: takeUntilDestroyed (Angular 16+)
private destroyRef = inject(DestroyRef);
ngOnInit() {
this.userService.users$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(users => this.users = users);
}Nested subscribes instead of flattening operatorsSubscribes aninhados em vez de operadores de flattening
// ✗ Nested subscribe — creates race conditions and leaks// ✗ Subscribe aninhado — cria race conditions e vazamentos
this.route.params.subscribe(params => {
this.userService.getUser(params['id']).subscribe(user => {
this.user = user;
});
});
// ✓ switchMap cancels the inner observable when a new outer value arrives// ✓ switchMap cancela o observable interno quando um novo valor externo chega
user$ = this.route.params.pipe(
switchMap(params => this.userService.getUser(params['id']))
);Wrong flattening operatorOperador de flattening errado
The four flattening operators are not interchangeable. Using the wrong one causes race conditions or dropped emissions silently:Os quatro operadores de flattening não são intercambiáveis. Usar o errado causa race conditions ou emissões descartadas silenciosamente:
// switchMap — cancels previous inner obs on new emission. Use: search, route params
// mergeMap — runs all inner obs concurrently. Use: parallel, order doesn't matter
// concatMap — queues inner obs sequentially. Use: ordered writes, upload queue
// exhaustMap — ignores new emissions while inner obs is active. Use: login button
// switchMap — cancela obs interno anterior na nova emissão. Use: busca, route params
// mergeMap — executa todos os obs internos concorrentemente. Use: paralelo, ordem não importa
// concatMap — enfileira obs internos sequencialmente. Use: escritas ordenadas, fila de upload
// exhaustMap — ignora novas emissões enquanto obs interno está ativo. Use: botão de login
// ✗ mergeMap on a search input — all requests run, responses arrive out of order// ✗ mergeMap em input de busca — todas as requisições executam, respostas chegam fora de ordem
searchTerm$.pipe(mergeMap(term => this.api.search(term)))
// ✓ switchMap cancels stale requests automatically// ✓ switchMap cancela requisições obsoletas automaticamente
searchTerm$.pipe(debounceTime(300), switchMap(term => this.api.search(term)))Sharing expensive observablesCompartilhando observables custosos
// ✗ Each subscriber triggers a new HTTP call// ✗ Cada subscriber aciona uma nova chamada HTTP
users$ = this.http.get<User[]>('/api/users');
// ✓ shareReplay multicasts to all subscribers, replays last value to late subscribers// ✓ shareReplay faz multicast para todos os subscribers, repete o último valor para tardios
users$ = this.http.get<User[]>('/api/users').pipe(
shareReplay({ bufferSize: 1, refCount: true })
);Mixing subscribe with async/awaitMisturando subscribe com async/await
Calling firstValueFrom() or lastValueFrom() to bridge RxJS and async/await is fine for one-shot operations. But mixing subscription patterns in the same component creates inconsistent teardown logic. Pick one style per component and be consistent.Chamar firstValueFrom() ou lastValueFrom() para fazer bridge entre RxJS e async/await é válido para operações one-shot. Mas misturar padrões de assinatura no mesmo componente cria lógica de teardown inconsistente. Escolha um estilo por componente e seja consistente.
4. Signals: The New State Primitive4. Signals: O Novo Primitivo de Estado
Angular 16+ introduced Signals as a fine-grained reactivity primitive. Unlike observables, signals are synchronous, don't need unsubscription, and integrate natively with change detection — a component using a signal automatically re-renders when the signal changes, with no async pipe needed.Angular 16+ introduziu Signals como um primitivo de reatividade granular. Ao contrário dos observables, signals são síncronos, não precisam de cancelamento de assinatura e se integram nativamente com a detecção de mudanças — um componente usando um signal re-renderiza automaticamente quando o signal muda, sem necessidade de pipe async.
// Signals for local component state — cleaner than BehaviorSubject// Signals para estado local de componente — mais limpo que BehaviorSubject
@Component({
template: `
<p>Count: {{ count() }}</p>
<p>Double: {{ double() }}</p>
<button (click)="increment()">+</button>
`
})
export class CounterComponent {
count = signal(0);
double = computed(() => this.count() * 2);
increment() { this.count.update(c => c + 1); }
}
// toSignal bridges RxJS observables to signals — best of both worlds// toSignal faz bridge de observables RxJS para signals — o melhor dos dois mundos
users = toSignal(this.userService.users$, { initialValue: [] });When to use Signals vs. Observables: Signals are ideal for local component state, derived UI values, and anything that doesn't involve async streams. Observables remain the right tool for HTTP calls, WebSocket streams, timer-based flows, and complex async transformations. toSignal() and toObservable() bridge the two where needed.Quando usar Signals vs. Observables: Signals são ideais para estado local de componente, valores derivados de UI e qualquer coisa que não envolva streams assíncronos. Observables continuam sendo a ferramenta certa para chamadas HTTP, streams WebSocket, fluxos baseados em timer e transformações assíncronas complexas. toSignal() e toObservable() fazem bridge entre os dois quando necessário.
5. Standalone Components and Feature-Based Structure5. Componentes Standalone e Estrutura por Feature
Since Angular 14, components can be standalone — no NgModule required. Angular 17+ makes standalone the default. This removes one of the most common Angular pain points (where to declare a component) and makes lazy loading trivial.Desde o Angular 14, componentes podem ser standalone — sem necessidade de NgModule. Angular 17+ torna standalone o padrão. Isso remove um dos pontos de dor mais comuns no Angular (onde declarar um componente) e torna o lazy loading trivial.
// Feature-based folder structure (not layer-based)// Estrutura de pastas por feature (não por camada)
src/app/
├── features/
│ ├── users/
│ │ ├── components/ # dumb components local to this feature# componentes dumb locais desta feature
│ │ ├── pages/ # smart components (routable)# componentes smart (roteáveis)
│ │ ├── services/ # data access, business logic# acesso a dados, lógica de negócio
│ │ ├── models/ # types and interfaces# types e interfaces
│ │ └── users.routes.ts # lazy-loaded route config# config de rota com lazy load
│ └── orders/
├── shared/ # components/pipes/directives used across features# componentes/pipes/diretivas usados em múltiplas features
└── core/ # singleton services, guards, interceptors# serviços singleton, guards, interceptors
// Lazy loading a standalone component — one line// Lazy loading de um componente standalone — uma linha
{ path: 'users', loadChildren: () => import('./features/users/users.routes') }Feature-based structure scales better than layer-based (components/, services/, models/ at the root). When a feature grows, everything related to it lives in one place. When you remove a feature, you delete one folder.Estrutura por feature escala melhor do que por camada (components/, services/, models/ na raiz). Quando uma feature cresce, tudo relacionado a ela vive em um lugar. Quando você remove uma feature, deleta uma pasta.
6. Services and Dependency Injection6. Serviços e Injeção de Dependência
// ✓ providedIn: 'root' — singleton, tree-shakeable if unused// ✓ providedIn: 'root' — singleton, tree-shakeable se não usado
@Injectable({ providedIn: 'root' })
export class UserService { ... }
// ✓ inject() function — cleaner than constructor injection, works in standalone// ✓ função inject() — mais limpa que injeção por construtor, funciona em standalone
@Component({ ... })
export class UserPageComponent {
private userService = inject(UserService);
private router = inject(Router);
}
// ✓ Feature-scoped service — provided at the feature level, not root// ✓ Serviço com escopo de feature — fornecido no nível da feature, não na raiz
@Injectable() // no providedIn// sem providedIn
export class CartService { ... }
// Provided in the feature route: { providers: [CartService] }// Fornecido na rota da feature: { providers: [CartService] }7. Performance: What Actually Moves the Needle7. Performance: O Que Realmente Faz Diferença
loadChildren or loadComponent on every non-root route.O bundle inicial deve conter apenas o shell (nav, componente raiz). Cada feature carregada sob demanda. loadChildren ou loadComponent em cada rota não raiz.trackBy, Angular re-creates every DOM node on list updates even if only one item changed. Always provide a unique key.Sem trackBy, o Angular recria cada nó DOM em atualizações de lista mesmo que apenas um item tenha mudado. Sempre forneça uma chave única.{{ computeTotal() }} is called on every change detection cycle. Use computed(), a pure pipe, or a property instead. Functions in templates are one of the most common Angular performance bugs.{{ computeTotal() }} é chamado em cada ciclo de detecção de mudanças. Use computed(), um pipe puro ou uma propriedade no lugar. Funções em templates são um dos bugs de performance mais comuns no Angular.@defer block loads a component's bundle only when triggered (viewport, interaction, idle). Charts, rich text editors, and third-party widgets are prime candidates.O bloco @defer do Angular 17+ carrega o bundle de um componente apenas quando acionado (viewport, interação, idle). Gráficos, editores de texto rico e widgets de terceiros são candidatos ideais.8. State Management: When to Reach for NgRx8. Gerenciamento de Estado: Quando Usar NgRx
NgRx is powerful and costly. The boilerplate is justified when: multiple unrelated components share the same state, state changes need to be auditable (time-travel debugging), or the team is large enough that unstructured state leads to conflicts. For everything else, services with Signals or BehaviorSubjects are sufficient and far simpler.NgRx é poderoso e custoso. O boilerplate é justificado quando: múltiplos componentes não relacionados compartilham o mesmo estado, mudanças de estado precisam ser auditáveis (time-travel debugging) ou o time é grande o suficiente para que estado não estruturado leve a conflitos. Para todo o resto, serviços com Signals ou BehaviorSubjects são suficientes e muito mais simples.
// Lightweight state service with Signals — covers 80% of use cases// Serviço de estado leve com Signals — cobre 80% dos casos de uso
@Injectable({ providedIn: 'root' })
export class CartStateService {
private items = signal<CartItem[]>([]);
readonly count = computed(() => this.items().length);
readonly total = computed(() =>
this.items().reduce((sum, item) => sum + item.price * item.qty, 0)
);
add(item: CartItem) { this.items.update(list => [...list, item]); }
remove(id: string) { this.items.update(list => list.filter(i => i.id !== id)); }
readonly items$ = toObservable(this.items); // bridge to RxJS when needed// bridge para RxJS quando necessário
}9. Testing Strategy9. Estratégia de Testes
Angular's TestBed is thorough but slow. A pragmatic testing strategy distinguishes three levels:O TestBed do Angular é completo, mas lento. Uma estratégia de testes pragmática distingue três níveis:
// Level 1: Pure unit tests — no TestBed, fast, for services and pure logic// Nível 1: Testes unitários puros — sem TestBed, rápido, para serviços e lógica pura
it('calculates total correctly', () => {
const service = new CartStateService();
service.add({ id: '1', price: 10, qty: 2 });
expect(service.total()).toBe(20);
});
// Level 2: Shallow component tests — TestBed but NO_ERRORS_SCHEMA for children// Nível 2: Testes de componente shallow — TestBed mas NO_ERRORS_SCHEMA para filhos
await TestBed.configureTestingModule({
imports: [UserCardComponent],
schemas: [NO_ERRORS_SCHEMA] // don't render child components// não renderiza componentes filhos
}).compileComponents();
// Level 3: Integration — for critical user flows (login, checkout) with real child tree// Nível 3: Integração — para fluxos críticos de usuário (login, checkout) com árvore de filhos realThe most common Angular testing mistake is over-relying on TestBed for everything. Dumb components don't need TestBed — just instantiate them, pass inputs, and assert on outputs. Reserve TestBed for components that interact with routing, DI, or the DOM in ways that require Angular's machinery.O erro de teste Angular mais comum é depender excessivamente do TestBed para tudo. Componentes dumb não precisam de TestBed — apenas instancie-os, passe inputs e verifique outputs. Reserve TestBed para componentes que interagem com roteamento, DI ou o DOM de formas que requerem a maquinaria do Angular.
10. HTTP Interceptors: The Right Place for Cross-Cutting Concerns10. HTTP Interceptors: O Lugar Certo para Preocupações Transversais
Interceptors sit between your code and every HTTP request/response. They're the correct place for auth token injection, error handling, loading indicators, and request logging — concerns that are universal but shouldn't pollute service methods. Angular 15+ supports functional interceptors, which are cleaner and don't require class boilerplate.Interceptors ficam entre seu código e cada requisição/resposta HTTP. São o lugar correto para injeção de token de autenticação, tratamento de erros, indicadores de loading e logging de requisições — preocupações universais que não devem poluir os métodos de serviço. Angular 15+ suporta interceptors funcionais, que são mais limpos e não requerem boilerplate de classe.
// auth.interceptor.ts — functional style (Angular 15+)// auth.interceptor.ts — estilo funcional (Angular 15+)
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = inject(AuthService).getToken();
if (!token) return next(req);
return next(req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }));
};
// error.interceptor.ts — global error handling with toast notifications// error.interceptor.ts — tratamento global de erros com notificações toast
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
const toast = inject(ToastService);
return next(req).pipe(
catchError((err: HttpErrorResponse) => {
if (err.status === 401) inject(AuthService).logout();
else if (err.status >= 500) toast.error('Server error. Please try again.');
return throwError(() => err);
})
);
};
// loading.interceptor.ts — global loading state// loading.interceptor.ts — estado global de loading
export const loadingInterceptor: HttpInterceptorFn = (req, next) => {
const loading = inject(LoadingService);
loading.start();
return next(req).pipe(finalize(() => loading.stop()));
};
// Register in app.config.ts// Registre em app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withInterceptors([authInterceptor, errorInterceptor, loadingInterceptor]))
]
};11. Route Guards and Resolvers11. Route Guards e Resolvers
Route guards control navigation access; resolvers pre-fetch data before a route activates. Both use functional form in Angular 15+. A common mistake is fetching data in ngOnInit() and showing a loading spinner — using a resolver instead means the component receives its data already populated, with no loading state to manage.Route guards controlam o acesso à navegação; resolvers pré-carregam dados antes que uma rota seja ativada. Ambos usam forma funcional no Angular 15+. Um erro comum é buscar dados em ngOnInit() e mostrar um spinner de loading — usar um resolver significa que o componente recebe seus dados já populados, sem estado de loading para gerenciar.
// auth.guard.ts — functional guard (Angular 15+)// auth.guard.ts — guard funcional (Angular 15+)
export const authGuard: CanActivateFn = () => {
const auth = inject(AuthService);
const router = inject(Router);
return auth.isAuthenticated() ? true : router.createUrlTree(['/login']);
};
// role.guard.ts — with parameterized roles// role.guard.ts — com roles parametrizadas
export const roleGuard = (requiredRole: string): CanActivateFn => () => {
const auth = inject(AuthService);
return auth.hasRole(requiredRole) ? true : inject(Router).createUrlTree(['/forbidden']);
};
// user.resolver.ts — data is ready when component activates// user.resolver.ts — dados prontos quando o componente ativa
export const userResolver: ResolveFn<User> = (route) =>
inject(UserService).getUser(route.paramMap.get('id')!);
// Route configuration// Configuração de rota
{
path: 'users/:id',
component: UserDetailPage,
canActivate: [authGuard, roleGuard('admin')],
resolve: { user: userResolver } // available as route.data['user']// disponível como route.data['user']
}12. Reactive Forms: Validation That Scales12. Reactive Forms: Validação que Escala
Reactive Forms give you a typed, testable model of your form state. They handle complex validation scenarios — cross-field validation, async validators (email availability check), dynamic form arrays — that template-driven forms struggle with. The key: define the form structure and its validators in the component class, not in the template.Reactive Forms fornecem um modelo tipado e testável do estado do formulário. Eles lidam com cenários de validação complexos — validação entre campos, validadores assíncronos (verificação de disponibilidade de email), arrays de formulário dinâmicos — com os quais formulários template-driven têm dificuldade. A chave: defina a estrutura do formulário e seus validadores na classe do componente, não no template.
// ✓ Strongly typed form with cross-field and async validators// ✓ Formulário fortemente tipado com validadores entre campos e assíncronos
form = new FormGroup({
email: new FormControl('', [Validators.required, Validators.email],
[this.emailAvailableValidator()]), // async// assíncrono
password: new FormControl('', [Validators.required, Validators.minLength(8)]),
confirmPassword: new FormControl('', Validators.required),
}, { validators: passwordMatchValidator }); // cross-field// entre campos
// Custom cross-field validator// Validador personalizado entre campos
const passwordMatchValidator: ValidatorFn = (group) => {
const pw = group.get('password')?.value;
const confirm = group.get('confirmPassword')?.value;
return pw === confirm ? null : { passwordMismatch: true };
};
// Async validator — debounced HTTP call// Validador assíncrono — chamada HTTP com debounce
emailAvailableValidator(): AsyncValidatorFn {
return (ctrl) => timer(400).pipe(
switchMap(() => this.authService.checkEmail(ctrl.value)),
map(taken => taken ? { emailTaken: true } : null),
catchError(() => of(null))
);
}
// FormArray — dynamic list of items// FormArray — lista dinâmica de itens
items = new FormArray<FormGroup>([]);
addItem() {
this.items.push(new FormGroup({
name: new FormControl('', Validators.required),
quantity: new FormControl(1, [Validators.required, Validators.min(1)])
}));
}13. Common Mistakes That Compound Over Time13. Erros Comuns que se Agravam com o Tempo
- Shared NgModules that import too much. A
SharedModulethat re-exports 30 components and all of Angular Material means every feature that imports it pays the full cost. Prefer fine-grained imports in standalone components.NgModules compartilhados que importam demais. UmSharedModuleque re-exporta 30 componentes e todo o Angular Material significa que cada feature que o importa paga o custo total. Prefira importações granulares em componentes standalone. - Services with state and side effects mixed together. A service that both fetches data and stores it as mutable fields is hard to test and makes race conditions likely. Separate data fetching (stateless) from state management (stateful).Serviços com estado e side effects misturados. Um serviço que tanto busca dados quanto os armazena como campos mutáveis é difícil de testar e torna race conditions prováveis. Separe busca de dados (sem estado) do gerenciamento de estado (com estado).
- Logic in HTML templates. Ternaries are fine; complex conditionals, array operations, and method calls in templates are not. Move logic to the component class or a pipe where it's testable.Lógica em templates HTML. Ternários são bons; condicionais complexas, operações de array e chamadas de métodos em templates não são. Mova a lógica para a classe do componente ou um pipe onde é testável.
- Ignoring hydration in SSR apps. Angular Universal apps that don't configure hydration correctly destroy and rebuild the full DOM on client-side bootstrap, negating SSR's performance benefit and causing layout flashes.Ignorar hidratação em apps SSR. Apps Angular Universal que não configuram a hidratação corretamente destroem e reconstroem o DOM completo no bootstrap client-side, negando o benefício de performance do SSR e causando flashes de layout.
Key TakeawaysPontos Principais
- Smart/dumb split is the most valuable component pattern. Dumb components are testable in isolation and reusable by definition. Treat any component that injects a service as a smell unless it's explicitly a container.A separação smart/dumb é o padrão de componente mais valioso. Componentes dumb são testáveis em isolamento e reutilizáveis por definição. Trate qualquer componente que injeta um serviço como um smell a menos que seja explicitamente um container.
OnPushon every component,trackByon every list. These two together eliminate the most common Angular performance issues and should be team standards enforced by linting.OnPushem todo componente,trackByem toda lista. Esses dois juntos eliminam os problemas de performance Angular mais comuns e devem ser padrões do time aplicados por linting.- Learn the four RxJS flattening operators and use the right one. The wrong operator causes silent bugs.
switchMapfor user-driven requests,exhaustMapfor submit buttons,concatMapfor ordered queues,mergeMapfor fire-and-forget parallel flows.Aprenda os quatro operadores de flattening RxJS e use o correto. O operador errado causa bugs silenciosos.switchMappara requisições guiadas pelo usuário,exhaustMappara botões de submit,concatMappara filas ordenadas,mergeMappara fluxos paralelos fire-and-forget. - Signals replace BehaviorSubject for local state. They're simpler, don't need unsubscription, and integrate directly with Angular's change detection. Adopt them for new component state; keep Observables for async streams.Signals substituem BehaviorSubject para estado local. São mais simples, não precisam de cancelamento de assinatura e se integram diretamente com a detecção de mudanças do Angular. Adote-os para novo estado de componente; mantenha Observables para streams assíncronos.
- Reach for NgRx only when services with Signals aren't enough. The boilerplate is real. A well-designed service with Signals handles most state management needs without the complexity.Recorra ao NgRx apenas quando serviços com Signals não forem suficientes. O boilerplate é real. Um serviço bem projetado com Signals lida com a maioria das necessidades de gerenciamento de estado sem a complexidade.