Angular Signals dan Computed di 2026: Reaktivitas Fine-Grained dan Pertanyaan Interview

Pelajari Angular Signals dan computed signals untuk reaktivitas fine-grained di Angular 20+. Panduan lengkap mencakup signal primitives, linkedSignal, interoperabilitas RxJS, dan pertanyaan interview teknis.

Angular Signals dan Computed - Reaktivitas Fine-Grained di 2026

Angular Signals merepresentasikan perubahan paling signifikan dalam model reaktivitas Angular sejak framework ini pertama kali dibuat. Diperkenalkan di Angular 17 dan sekarang menjadi pendekatan default di Angular 20+, signals menyediakan reaktivitas fine-grained yang menghilangkan siklus change detection yang tidak diperlukan dan terintegrasi secara mulus dengan aplikasi zoneless.

Poin Penting untuk Interview

Signals adalah primitif reaktif yang sinkron dan bebas glitch. Computed signal menghitung ulang hanya ketika dependensinya berubah dan hanya sekali per siklus perubahan, terlepas dari berapa kali signal tersebut dibaca.

Memahami Signal Primitives di Angular 20

Sistem signal Angular terdiri dari tiga primitif inti: signal(), computed(), dan effect(). Masing-masing memiliki tujuan berbeda dalam reactive graph.

Writable signal menyimpan nilai yang dapat diperbarui dengan .set() atau .update(). Membaca signal mengembalikan nilai saat ini, dan computed signal atau effect yang bergantung padanya akan diberitahu tentang perubahan.

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 dengan nilai awal
  count = signal(0);
  
  // Computed signal diturunkan dari count
  // Hanya menghitung ulang ketika count berubah
  doubleCount = computed(() => this.count() * 2);
  
  increment(): void {
    // .update() menerima nilai saat ini sebagai argumen
    this.count.update(c => c + 1);
  }
}

Perbedaan utama dari RxJS BehaviorSubject: signals bersifat sinkron dan bebas glitch. Ketika count berubah, doubleCount menghitung ulang tepat sekali, bahkan jika dibaca beberapa kali dalam blok sinkron yang sama.

Computed Signals: Evaluasi Lazy dan Memoization

Computed signals meng-cache hasilnya sampai dependency berubah. Memoization ini otomatis, tidak seperti RxJS di mana shareReplay atau distinctUntilChanged harus ditambahkan secara manual.

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 hanya melacak properti yang dibaca
  totalSpent = computed(() => {
    const purchases = this.user().purchases;
    return purchases.reduce((sum, p) => sum + p, 0);
  });
  
  // Bergantung pada computed signal lain
  averagePurchase = computed(() => {
    const total = this.totalSpent();
    const count = this.user().purchases.length;
    return count > 0 ? total / count : 0;
  });
}

Computed signal mengevaluasi secara lazy: fungsi computation berjalan hanya ketika signal pertama kali dibaca, bukan ketika dideklarasikan. Pembacaan berikutnya mengembalikan nilai yang di-cache sampai dependency membatalkannya.

Effect: Side Effects dalam Reactive Graph

Effects mengeksekusi kode sebagai respons terhadap perubahan signal. Effects berjalan secara asinkron selama siklus change detection, setelah semua update sinkron selesai.

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 menyinkronkan state signal ke atribut DOM
    effect(() => {
      const theme = this.isDarkMode() ? 'dark' : 'light';
      this.document.documentElement.setAttribute('data-theme', theme);
    });
    
    // Effect untuk persistensi localStorage
    effect(() => {
      localStorage.setItem('theme', this.isDarkMode() ? 'dark' : 'light');
    });
  }
  
  toggleTheme(): void {
    this.isDarkMode.update(dark => !dark);
  }
}

Effects melacak dependencies secara dinamis. Jika cabang kondisional tidak diambil, signals yang dibaca hanya di cabang tersebut tidak dilacak sampai kondisi berubah.

Anti-Pattern: Effect untuk Derivasi State

Menggunakan effect untuk menyalin data dari satu signal ke signal lain menunjukkan masalah desain. Lebih baik gunakan computed untuk derived state atau linkedSignal ketika nilai turunan perlu dapat ditulis.

linkedSignal: Writable Derived State

Diperkenalkan di Angular 19 dan stabil di Angular 20, linkedSignal membuat writable signal yang di-reset ketika sumbernya berubah. Ini menyelesaikan skenario di mana computed bersifat read-only tetapi nilai turunan memerlukan modifikasi lokal.

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 di-reset ke 1 ketika pageSize atau totalItems berubah
  // tetapi memungkinkan navigasi manual 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));
  }
}

Tanpa linkedSignal, pola ini memerlukan effect yang menulis ke signal lain (anti-pattern) atau orkestrasi RxJS yang kompleks.

Siap menguasai wawancara Angular Anda?

Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.

Signal-Based Component APIs: input, model, viewChild

Angular 20 mempromosikan signal-based component APIs ke status stabil. Ini menggantikan decorators dengan alternatif reaktif yang terintegrasi ke dalam signal graph.

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 menggantikan @Input()
  firstName = input.required<string>();
  lastName = input<string>('');
  
  // model() membuat signal yang dapat di-bind dua arah
  // Parent menggunakan [(selected)]="parentSignal"
  selected = model(false);
  
  // Output emitter untuk events
  cardClicked = output<void>();
  
  // viewChild mengembalikan Signal<ElementRef | undefined>
  cardElement = viewChild<ElementRef>('card');
  
  // Computed diturunkan dari signal inputs
  fullName = computed(() => 
    `${this.firstName()} ${this.lastName()}`.trim()
  );
  
  toggle(): void {
    this.selected.update(s => !s);
    this.cardClicked.emit();
  }
}

Signal inputs memberikan type safety yang lebih baik: input.required() mengembalikan InputSignal<T> sementara input() mengembalikan InputSignal<T | undefined> kecuali default disediakan.

Signals vs RxJS: Performa di Bawah Beban Berat

Benchmark di Angular 21+ menunjukkan bahwa signals mengurangi overhead change detection dalam skenario UI-heavy. Di bawah beban berat dengan update yang sering, signals menunjukkan profil memori yang lebih bersih dan penggunaan CPU yang lebih rendah dibandingkan dengan binding berbasis RxJS.

Pemisahan concern jelas:

Use CasePendekatan yang Direkomendasikan
State komponen lokalsignal()
Nilai UI turunancomputed()
Sinkronisasi ke API eksternaleffect()
HTTP requestsRxJS Observable + toSignal()
WebSocket streamsRxJS dengan operators
Input user dengan debounceRxJS debounceTime, lalu toSignal()
Orkestrasi async kompleksRxJS pipelines

RxJS tetap menjadi tool untuk async streams, cancellation, dan backpressure. Signals menangani state sinkron.

Interoperabilitas RxJS: toSignal dan toObservable

Angular menyediakan utilitas di @angular/core/rxjs-interop untuk menjembatani signals dan 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('');
  
  // Konversi signal ke observable untuk operator RxJS
  private query$ = toObservable(this.query);
  
  // RxJS pipeline dengan debounce, lalu kembali ke 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 subscribe ketika dibuat dan unsubscribe ketika komponen di-destroy. Opsi initialValue menghilangkan type undefined dari signal.

Pertanyaan Interview: Angular Signals

Interview teknis di 2026 sering menguji pemahaman signal. Berikut adalah pola yang membedakan kandidat senior.

Q: Apa yang terjadi jika computed signal melempar error?

Error menyebar ke effect atau computed mana pun yang membacanya. Angular tidak meng-cache errors, computation mencoba ulang pada pembacaan berikutnya. Kode production harus menangani errors di dalam fungsi computed.

Q: Bisakah menulis ke signal di dalam computed?

Tidak. Mencoba memanggil .set() atau .update() di dalam computed melempar error. Fungsi computed harus pure. Gunakan linkedSignal jika memerlukan nilai turunan yang juga writable.

Q: Bagaimana effects menangani pembacaan signal dalam kondisional?

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

effect(() => {
  if (showDetails()) {
    // userDetails hanya dilacak ketika showDetails true
    console.log('Details:', userDetails().email);
  }
});

Effects melacak dependencies berdasarkan eksekusi terbaru. Jika showDetails adalah false, perubahan pada userDetails tidak memicu effect.

Q: Bagaimana equality signal bekerja?

Secara default, signals menggunakan Object.is untuk equality. Untuk objects, ini berarti reference equality. Custom equality dapat disediakan:

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

Kandidat yang menyebutkan bahwa computed signals di-memoize dan dievaluasi secara lazy menunjukkan pemahaman di luar penggunaan API dasar. Mendiskusikan kapan menggunakan linkedSignal vs computed menunjukkan keakraban dengan pola Angular 19+.

Change Detection: Signals dan Zoneless Angular

Angular 22 default ke zoneless change detection untuk proyek baru. Signals adalah pusat dari ini: signals memberitahu Angular dengan tepat kapan state berubah, menghilangkan kebutuhan Zone.js untuk mem-patch async APIs.

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 {
    // Update signal memicu change detection
    // Tidak memerlukan Zone.js
    this.count.update(c => c + 1);
  }
}

Dengan signals, Angular melacak dengan tepat komponen mana yang bergantung pada state mana. Update signal menjadwalkan change detection hanya untuk komponen yang terpengaruh, bukan seluruh tree.

Resource API: Async Data sebagai Signals

Resource API Angular 20 (resource() dan rxResource()) secara deklaratif memuat async data ke signals, menggantikan pola subscription 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);
  
  // Resource async deklaratif
  // Refetch ketika userId berubah
  userResource = rxResource({
    request: () => this.userId(),
    loader: ({ request: id }) => 
      this.http.get<User>(`/api/users/${id}`)
  });
  
  loadUser(id: number): void {
    this.userId.set(id);
  }
}

Resource secara otomatis melacak loading state, errors, dan nilai yang di-resolve. Resource melakukan refetch ketika request signal berubah.

Best Practices untuk Arsitektur Signal

Menstruktur signals dalam aplikasi yang lebih besar memerlukan disiplin. Pola-pola ini dapat digunakan dari komponen hingga services.

Colocate signals yang terkait: Kelompokkan signals yang berubah bersama. Komponen form menyimpan field signals-nya bersama, tidak tersebar di services.

Angkat shared state ke services: Ketika beberapa komponen membutuhkan state yang sama, pindahkan signal ke injectable service:

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[]>([]);
  
  // Ekspos read-only signal ke 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 }];
    });
  }
}

Lebih memilih computed daripada effect: Jika nilai dapat diturunkan, gunakan computed. Effects adalah untuk side effects yang meninggalkan reactive graph: manipulasi DOM, localStorage, analytics, network requests.

Migrasi dari RxJS ke Signals

Codebase Angular legacy dapat bermigrasi secara bertahap. Utilitas interop RxJS memungkinkan signals dan observables untuk hidup berdampingan.

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

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

Untuk operasi async, pertahankan RxJS untuk pipeline dan konversi di boundary:

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

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

Mulai berlatih!

Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.

Persiapan Pertanyaan Interview Angular Signals

  • Signals adalah primitif sinkron; RxJS menangani async streams
  • Computed signals bersifat lazy dan di-memoize, menghitung ulang hanya ketika dependencies berubah
  • Effects berjalan secara asinkron selama change detection, bukan segera
  • linkedSignal menyelesaikan masalah writable-derived-state tanpa effects
  • Signal inputs (input(), model()) menggantikan decorators dengan alternatif reaktif
  • toSignal dan toObservable menjembatani signals dan RxJS untuk migrasi bertahap
  • Zoneless Angular bergantung pada signals untuk fine-grained change detection
  • Resource API (rxResource) mengelola async state secara deklaratif
  • Panduan resmi Angular signals tetap menjadi referensi otoritatif untuk detail API dan best practices
Tantangan harian

Bisakah kamu menemukan bug di Angular?

Satu potongan kode nyata, satu bug tersembunyi, satu percobaan per hari. Tanpa akun untuk mencoba.

Anthony Fillion-Maillet

Ditulis oleh

Anthony Fillion-Maillet

Pendiri SharpSkill

Developer fullstack selama lebih dari 10 tahun. Ia menjalankan SharpSkill dan bertanggung jawab atas semua yang diterbitkan di sini.

Diperbarui 17 September 2026

Tag

#angular
#signals
#computed
#reactivity
#interview

Bagikan

Artikel terkait