Angular Signals e Computed em 2026: Reatividade Granular e Perguntas de Entrevista

Dominar Signals do Angular, computed, effect e linkedSignal no Angular 20+. Padrões de reatividade fina e preparação para entrevistas técnicas.

Angular Signals e Computed - Reatividade Granular

Os Signals do Angular representam a mudança mais significativa no modelo de reatividade do framework desde sua criação. Introduzidos no Angular 17 e agora a abordagem padrão no Angular 20+, os signals fornecem reatividade granular que elimina ciclos desnecessários de detecção de mudanças e integra-se perfeitamente com aplicações zoneless.

Ponto-Chave para Entrevistas

Os signals são primitivas reativas síncronas e livres de glitches. Um signal computed recalcula apenas quando suas dependências mudam e apenas uma vez por ciclo de mudança, independentemente de quantas vezes seja lido.

Entendendo as Primitivas Signal no Angular 20

O sistema de signals do Angular consiste em três primitivas principais: signal(), computed() e effect(). Cada uma serve a um propósito distinto no grafo reativo.

Um signal gravável contém um valor que pode ser atualizado com .set() ou .update(). A leitura do signal retorna o valor atual, e quaisquer signals computed ou effects que dependam dele são notificados sobre as mudanças.

counter.component.tstypescript
import { Component, signal, computed } from '@angular/core';

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <p>Count: {{ count() }}</p>
    <p>Double: {{ doubleCount() }}</p>
    <button (click)="increment()">+1</button>
  `
})
export class CounterComponent {
  // Writable signal with initial value
  count = signal(0);
  
  // Computed signal derives from count
  // Only recalculates when count changes
  doubleCount = computed(() => this.count() * 2);
  
  increment(): void {
    // .update() receives current value as argument
    this.count.update(c => c + 1);
  }
}

A diferença principal do BehaviorSubject do RxJS: signals são síncronos e livres de glitches. Quando count muda, doubleCount recalcula exatamente uma vez, mesmo se lido múltiplas vezes no mesmo bloco síncrono.

Signals Computed: Avaliação Preguiçosa e Memoização

Signals computed armazenam em cache seu resultado até que uma dependência mude. Essa memoização é automática, diferentemente do RxJS onde shareReplay ou distinctUntilChanged devem ser adicionados manualmente.

user-stats.component.tstypescript
import { Component, signal, computed } from '@angular/core';

interface User {
  id: number;
  name: string;
  purchases: number[];
}

@Component({
  selector: 'app-user-stats',
  standalone: true,
  template: `
    <div>
      <p>User: {{ user().name }}</p>
      <p>Total spent: {{ totalSpent() | currency }}</p>
      <p>Average purchase: {{ averagePurchase() | currency }}</p>
    </div>
  `
})
export class UserStatsComponent {
  user = signal<User>({
    id: 1,
    name: 'Alice',
    purchases: [99.99, 149.50, 29.99]
  });
  
  // Computed tracks only the properties it reads
  totalSpent = computed(() => {
    const purchases = this.user().purchases;
    return purchases.reduce((sum, p) => sum + p, 0);
  });
  
  // Depends on another computed signal
  averagePurchase = computed(() => {
    const total = this.totalSpent();
    const count = this.user().purchases.length;
    return count > 0 ? total / count : 0;
  });
}

Um signal computed é avaliado de forma preguiçosa: a função de cálculo executa apenas quando o signal é lido pela primeira vez, não quando declarado. Leituras subsequentes retornam o valor em cache até que uma dependência o invalide.

Effect: Efeitos Colaterais no Grafo Reativo

Effects executam código em resposta a mudanças de signals. Eles executam de forma assíncrona durante o ciclo de detecção de mudanças, após todas as atualizações síncronas serem completadas.

theme-sync.component.tstypescript
import { Component, signal, effect, inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';

@Component({
  selector: 'app-theme-sync',
  standalone: true,
  template: `
    <button (click)="toggleTheme()">Toggle Theme</button>
  `
})
export class ThemeSyncComponent {
  private document = inject(DOCUMENT);
  
  isDarkMode = signal(false);
  
  constructor() {
    // Effect syncs signal state to DOM attribute
    effect(() => {
      const theme = this.isDarkMode() ? 'dark' : 'light';
      this.document.documentElement.setAttribute('data-theme', theme);
    });
    
    // Effect for localStorage persistence
    effect(() => {
      localStorage.setItem('theme', this.isDarkMode() ? 'dark' : 'light');
    });
  }
  
  toggleTheme(): void {
    this.isDarkMode.update(dark => !dark);
  }
}

Effects rastreiam dependências dinamicamente. Se um branch condicional não é tomado, signals lidos apenas naquele branch não são rastreados até que a condição mude.

Anti-Padrão: Effect para Derivação de Estado

Usar effect para copiar dados de um signal para outro indica um problema de design. É preferível usar computed para estado derivado ou linkedSignal quando o valor derivado precisa ser modificável.

linkedSignal: Estado Derivado Modificável

Introduzido no Angular 19 e estável no Angular 20, linkedSignal cria um signal modificável que reinicia quando sua fonte muda. Ele resolve cenários onde computed é somente leitura, mas o valor derivado precisa de modificação local.

pagination.component.tstypescript
import { Component, signal, linkedSignal, computed } from '@angular/core';

@Component({
  selector: 'app-pagination',
  standalone: true,
  template: `
    <select (change)="pageSize.set(+$any($event.target).value)">
      <option [value]="10">10 per page</option>
      <option [value]="25">25 per page</option>
      <option [value]="50">50 per page</option>
    </select>
    <p>Page {{ currentPage() }} of {{ totalPages() }}</p>
    <button (click)="prevPage()" [disabled]="currentPage() === 1">Prev</button>
    <button (click)="nextPage()" [disabled]="currentPage() === totalPages()">Next</button>
  `
})
export class PaginationComponent {
  totalItems = signal(243);
  pageSize = signal(10);
  
  totalPages = computed(() => 
    Math.ceil(this.totalItems() / this.pageSize())
  );
  
  // linkedSignal resets to 1 when pageSize or totalItems changes
  // but allows manual navigation via prevPage/nextPage
  currentPage = linkedSignal(() => 1);
  
  prevPage(): void {
    this.currentPage.update(p => Math.max(1, p - 1));
  }
  
  nextPage(): void {
    this.currentPage.update(p => Math.min(this.totalPages(), p + 1));
  }
}

Sem linkedSignal, esse padrão exigia um effect escrevendo para outro signal (anti-padrão) ou orquestração RxJS complexa.

Pronto para mandar bem nas entrevistas de Angular?

Pratique com nossos simuladores interativos, flashcards e testes tecnicos.

APIs de Componentes Baseadas em Signals: input, model, viewChild

Angular 20 promove as APIs de componentes baseadas em signals para status estável. Estas substituem decoradores por alternativas reativas que integram-se ao grafo de signals.

user-card.component.tstypescript
import { 
  Component, 
  input, 
  model, 
  output, 
  computed,
  viewChild,
  ElementRef
} from '@angular/core';

@Component({
  selector: 'app-user-card',
  standalone: true,
  template: `
    <div #card class="card" [class.selected]="selected()">
      <h3>{{ fullName() }}</h3>
      <button (click)="toggle()">{{ selected() ? 'Deselect' : 'Select' }}</button>
    </div>
  `
})
export class UserCardComponent {
  // Signal input replaces @Input()
  firstName = input.required<string>();
  lastName = input<string>('');
  
  // model() creates two-way bindable signal
  // Parent uses [(selected)]="parentSignal"
  selected = model(false);
  
  // Output emitter for events
  cardClicked = output<void>();
  
  // viewChild returns Signal<ElementRef | undefined>
  cardElement = viewChild<ElementRef>('card');
  
  // Computed derives from signal inputs
  fullName = computed(() => 
    `${this.firstName()} ${this.lastName()}`.trim()
  );
  
  toggle(): void {
    this.selected.update(s => !s);
    this.cardClicked.emit();
  }
}

Inputs baseados em signals fornecem melhor segurança de tipos: input.required() retorna InputSignal<T> enquanto input() retorna InputSignal<T | undefined> a menos que um valor padrão seja fornecido.

Signals vs RxJS: Performance Sob Carga

Benchmarks no Angular 21+ demonstram que signals reduzem a sobrecarga de detecção de mudanças em cenários intensivos de UI. Sob carga pesada com atualizações frequentes, signals mostraram perfis de memória mais limpos e menor uso de CPU comparado a bindings orientados por RxJS.

A separação de responsabilidades é clara:

Caso de UsoAbordagem Recomendada
Estado local do componentesignal()
Valores UI derivadoscomputed()
Sincronização com APIs externaseffect()
Requisições HTTPObservable RxJS + toSignal()
Streams WebSocketRxJS com operadores
Input de usuário com debounceRxJS debounceTime, depois toSignal()
Orquestração assíncrona complexaPipelines RxJS

RxJS continua sendo a ferramenta para streams assíncronas, cancelamento e contrapressão. Signals gerenciam estado síncrono.

Interoperabilidade RxJS: toSignal e toObservable

Angular fornece utilitários em @angular/core/rxjs-interop para fazer a ponte entre signals e observables.

search.component.tstypescript
import { Component, signal, inject } from '@angular/core';
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { debounceTime, switchMap, distinctUntilChanged } from 'rxjs';

interface SearchResult {
  id: number;
  title: string;
}

@Component({
  selector: 'app-search',
  standalone: true,
  template: `
    <input 
      [value]="query()" 
      (input)="query.set($any($event.target).value)"
      placeholder="Search..."
    />
    @if (results(); as items) {
      <ul>
        @for (item of items; track item.id) {
          <li>{{ item.title }}</li>
        }
      </ul>
    }
  `
})
export class SearchComponent {
  private http = inject(HttpClient);
  
  query = signal('');
  
  // Convert signal to observable for RxJS operators
  private query$ = toObservable(this.query);
  
  // RxJS pipeline with debounce, then back to signal
  results = toSignal(
    this.query$.pipe(
      debounceTime(300),
      distinctUntilChanged(),
      switchMap(q => q.length > 2 
        ? this.http.get<SearchResult[]>(`/api/search?q=${q}`)
        : []
      )
    ),
    { initialValue: [] as SearchResult[] }
  );
}

toSignal se inscreve quando criado e cancela a inscrição quando o componente é destruído. A opção initialValue elimina o tipo undefined do signal.

Perguntas de Entrevista: Angular Signals

Entrevistas técnicas em 2026 frequentemente testam o entendimento de signals. Estes são os padrões que distinguem candidatos seniores.

P: O que acontece se um signal computed lança um erro?

O erro se propaga para qualquer effect ou computed que o leia. Angular não armazena erros em cache, o cálculo tenta novamente na próxima leitura. Código de produção deve tratar erros dentro da função computed.

P: É possível escrever em um signal dentro de um computed?

Não. Tentar chamar .set() ou .update() dentro de um computed lança um erro. Funções computed devem ser puras. Deve-se usar linkedSignal se for necessário um valor derivado que também seja modificável.

P: Como effects tratam leituras de signals em condicionais?

typescript
// Dynamic dependency tracking example
const showDetails = signal(false);
const userDetails = signal({ name: 'Alice', email: 'alice@example.com' });

effect(() => {
  if (showDetails()) {
    // userDetails is only tracked when showDetails is true
    console.log('Details:', userDetails().email);
  }
});

Effects rastreiam dependências baseados na execução mais recente. Se showDetails é false, mudanças em userDetails não disparam o effect.

P: Como funciona a igualdade em signals?

Por padrão, signals usam Object.is para igualdade. Para objetos, isso significa igualdade de referência. Igualdade personalizada pode ser fornecida:

typescript
const user = signal(
  { id: 1, name: 'Alice' },
  { equal: (a, b) => a.id === b.id }
);
Dica de Entrevista

Candidatos que mencionam que signals computed são memoizados e avaliados de forma preguiçosa demonstram entendimento além do uso básico da API. Discutir quando usar linkedSignal vs computed mostra familiaridade com os padrões do Angular 19+.

Detecção de Mudanças: Signals e Angular Zoneless

Angular 22 usa por padrão detecção de mudanças zoneless para novos projetos. Signals são centrais para isso: eles notificam o Angular precisamente quando o estado muda, eliminando a necessidade do Zone.js de interceptar APIs assíncronas.

zoneless-counter.component.tstypescript
import { Component, signal, ChangeDetectionStrategy } from '@angular/core';

@Component({
  selector: 'app-zoneless-counter',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <p>Count: {{ count() }}</p>
    <button (click)="increment()">+1</button>
  `
})
export class ZonelessCounterComponent {
  count = signal(0);
  
  increment(): void {
    // Signal update triggers change detection
    // No Zone.js required
    this.count.update(c => c + 1);
  }
}

Com signals, Angular rastreia exatamente quais componentes dependem de qual estado. Uma atualização de signal agenda detecção de mudanças apenas para componentes afetados, não a árvore inteira.

API Resource: Dados Assíncronos como Signals

A API Resource do Angular 20 (resource() e rxResource()) carrega dados assíncronos de forma declarativa em signals, substituindo padrões de subscrição manual.

user-profile.component.tstypescript
import { Component, signal, computed } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';

interface User {
  id: number;
  name: string;
  email: string;
}

@Component({
  selector: 'app-user-profile',
  standalone: true,
  template: `
    @if (userResource.isLoading()) {
      <p>Loading...</p>
    }
    @if (userResource.error()) {
      <p>Error: {{ userResource.error() }}</p>
    }
    @if (userResource.value(); as user) {
      <h2>{{ user.name }}</h2>
      <p>{{ user.email }}</p>
    }
  `
})
export class UserProfileComponent {
  private http = inject(HttpClient);
  
  userId = signal(1);
  
  // Declarative async resource
  // Refetches when userId changes
  userResource = rxResource({
    request: () => this.userId(),
    loader: ({ request: id }) => 
      this.http.get<User>(`/api/users/${id}`)
  });
  
  loadUser(id: number): void {
    this.userId.set(id);
  }
}

O resource rastreia automaticamente estado de carregamento, erros e o valor resolvido. Ele recarrega quando o signal de requisição muda.

Melhores Práticas para Arquitetura Signal

Estruturar signals em aplicações maiores requer disciplina. Estes padrões escalam de componentes a serviços.

Colocalizar signals relacionados: Agrupar signals que mudam juntos. Um componente de formulário mantém seus signals de campos juntos, não espalhados em serviços.

Elevar estado compartilhado para serviços: Quando múltiplos componentes precisam do mesmo estado, mover o signal para um serviço injetável:

cart.service.tstypescript
import { Injectable, signal, computed } from '@angular/core';

interface CartItem {
  id: number;
  name: string;
  price: number;
  quantity: number;
}

@Injectable({ providedIn: 'root' })
export class CartService {
  private items = signal<CartItem[]>([]);
  
  // Expose read-only signal to consumers
  readonly cartItems = this.items.asReadonly();
  
  readonly totalPrice = computed(() =>
    this.items().reduce((sum, item) => sum + item.price * item.quantity, 0)
  );
  
  readonly itemCount = computed(() =>
    this.items().reduce((sum, item) => sum + item.quantity, 0)
  );
  
  addItem(item: Omit<CartItem, 'quantity'>): void {
    this.items.update(items => {
      const existing = items.find(i => i.id === item.id);
      if (existing) {
        return items.map(i => 
          i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
        );
      }
      return [...items, { ...item, quantity: 1 }];
    });
  }
}

Preferir computed a effects: Se um valor pode ser derivado, usar computed. Effects são para efeitos colaterais que saem do grafo reativo: manipulação do DOM, localStorage, analytics, requisições de rede.

Migrando de RxJS para Signals

Codebases Angular legadas podem migrar incrementalmente. Os utilitários de interoperabilidade RxJS permitem que signals e observables coexistam.

typescript
// Before: RxJS-based
@Component({ ... })
export class LegacyComponent {
  private userSubject = new BehaviorSubject<User | null>(null);
  user$ = this.userSubject.asObservable();
  
  userName$ = this.user$.pipe(
    map(u => u?.name ?? 'Guest')
  );
}

// After: Signal-based
@Component({ ... })
export class ModernComponent {
  user = signal<User | null>(null);
  
  userName = computed(() => this.user()?.name ?? 'Guest');
}

Para operações assíncronas, manter RxJS para o pipeline e converter na fronteira:

typescript
// Hybrid approach
private search$ = toObservable(this.searchQuery).pipe(
  debounceTime(300),
  switchMap(q => this.api.search(q))
);

results = toSignal(this.search$, { initialValue: [] });

Comece a praticar!

Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.

Preparação para Perguntas de Entrevista sobre Angular Signals

  • Signals são primitivas síncronas; RxJS trata streams assíncronas
  • Signals computed são preguiçosos e memoizados, recalculando apenas quando dependências mudam
  • Effects executam assincronamente durante detecção de mudanças, não imediatamente
  • linkedSignal resolve o problema de estado derivado modificável sem effects
  • Inputs signal (input(), model()) substituem decoradores por alternativas reativas
  • toSignal e toObservable fazem ponte entre signals e RxJS para migração incremental
  • Angular zoneless depende de signals para detecção de mudanças granular
  • A API Resource (rxResource) gerencia estado assíncrono de forma declarativa
  • O guia oficial de signals do Angular continua sendo a referência autoritativa para detalhes da API e melhores práticas
Desafio do dia

Você saberia encontrar o bug em Angular?

Um trecho real, um bug escondido, uma tentativa por dia. Sem conta para testar.

Anthony Fillion-Maillet

Escrito por

Anthony Fillion-Maillet

Fundador da SharpSkill

Desenvolvedor fullstack há mais de 10 anos. Dirige a SharpSkill e responde por tudo o que é publicado aqui.

Atualizado em 17 de setembro de 2026

Tags

#angular
#signals
#computed
#reactivity
#interview

Compartilhar

Artigos relacionados