# Angular 18: Signals dan Fitur-Fitur Baru > Pelajari Angular 18 Signals, deteksi perubahan zoneless, dan API berbasis signal baru untuk membangun aplikasi yang lebih performan. - Published: 2026-01-18 - Updated: 2026-04-10 - Author: SharpSkill - Tags: angular 18, angular signals, zoneless, signal inputs, reactivity - Reading time: 12 min --- Angular 18 menandai titik balik dalam evolusi framework ini dengan stabilisasi Signals. Primitif reaktif baru ini secara fundamental mengubah cara membangun komponen Angular, menawarkan alternatif modern dari dekorator tradisional sekaligus membuka jalan menuju deteksi perubahan tanpa Zone.js. > **Yang akan dipelajari** > > API berbasis signal di Angular 18: input(), model(), viewChild(), dan konfigurasi zoneless untuk aplikasi yang lebih ringan dan performan. ## Memahami Signals di Angular 18 Signals merepresentasikan pendekatan baru terhadap reaktivitas di Angular. Berbeda dengan dekorator klasik seperti `@Input()` yang bergantung pada deteksi perubahan Zone.js, Signals menawarkan reaktivitas yang presisi dan eksplisit. Setiap Signal mengenkapsulasi sebuah nilai dan secara otomatis memberi notifikasi kepada konsumen saat nilai tersebut berubah. Pendekatan ini memberikan beberapa keuntungan: performa lebih baik melalui pembaruan yang ditargetkan, integrasi native dengan fungsi `computed()` dan `effect()`, serta persiapan menuju masa depan zoneless Angular. ```typescript // signals-basics.component.ts // Demonstrasi konsep fundamental Signals import { Component, signal, computed, effect } from '@angular/core'; @Component({ selector: 'app-counter', standalone: true, template: `

Counter: {{ count() }}

Double: {{ doubleCount() }}

` }) export class CounterComponent { // Signal writable - nilai bisa dimodifikasi count = signal(0); // Signal computed - diturunkan secara otomatis dari count // Hanya dihitung ulang saat count berubah doubleCount = computed(() => this.count() * 2); constructor() { // Effect - dieksekusi setiap kali count berubah // Berguna untuk side effects (log, API call, dll.) effect(() => { console.log(`Nilai counter baru: ${this.count()}`); }); } increment() { // update() memungkinkan modifikasi berdasarkan nilai sebelumnya this.count.update(value => value + 1); } decrement() { this.count.update(value => value - 1); } reset() { // set() langsung mengganti nilai this.count.set(0); } } ``` Signals berfungsi sebagai kontainer reaktif: `signal()` membuat Signal yang dapat ditulis, `computed()` menurunkan nilai-nilai yang dihitung, dan `effect()` memungkinkan eksekusi aksi sebagai respons terhadap perubahan. ## Signal Inputs dengan input() Fungsi `input()` menggantikan dekorator `@Input()` tradisional. Fungsi ini mengembalikan `InputSignal` yang hanya-baca, memastikan data selalu mengalir dari parent ke child tanpa modifikasi yang tidak disengaja. ```typescript // book-card.component.ts // Komponen menggunakan signal inputs import { Component, input, computed } from '@angular/core'; interface Book { id: string; title: string; author: string; price: number; discountPercent?: number; } @Component({ selector: 'app-book-card', standalone: true, template: `

{{ book().title }}

By {{ book().author }}

@if (hasDiscount()) {

\${{ book().price }} \${{ discountedPrice() }}

} @else {

\${{ book().price }}

} @if (featured()) { Featured }
` }) export class BookCardComponent { // Input wajib - template tidak bisa dikompilasi tanpa prop ini book = input.required(); // Input opsional dengan nilai default featured = input(false); // Computed berdasarkan input - dihitung ulang secara otomatis hasDiscount = computed(() => { const discount = this.book().discountPercent; return discount !== undefined && discount > 0; }); // Perhitungan harga diskon discountedPrice = computed(() => { const { price, discountPercent } = this.book(); if (!discountPercent) return price; return (price * (100 - discountPercent) / 100).toFixed(2); }); } ``` Penggunaan di template parent tetap serupa, namun dengan keamanan tipe dan reaktivitas Signal: ```typescript // book-list.component.ts // Komponen parent menggunakan book-card import { Component, signal } from '@angular/core'; import { BookCardComponent } from './book-card.component'; @Component({ selector: 'app-book-list', standalone: true, imports: [BookCardComponent], template: `
@for (book of books(); track book.id) { }
` }) export class BookListComponent { books = signal([ { id: '1', title: 'Clean Code', author: 'Robert C. Martin', price: 35 }, { id: '2', title: 'The Pragmatic Programmer', author: 'David Thomas', price: 42, discountPercent: 15 } ]); featuredBookId = signal('1'); } ``` Perbedaan utama dengan `@Input()`: signal inputs bersifat hanya-baca. Memanggil `this.book.set()` dari komponen child tidak dimungkinkan, yang memperkuat aliran data searah. ## Two-Way Binding dengan model() Untuk kasus yang memerlukan sinkronisasi dua arah, Angular 18 memperkenalkan `model()`. Fungsi ini membuat Signal yang dapat ditulis dan secara otomatis mempropagasikan perubahan ke komponen parent. ```typescript // search-input.component.ts // Komponen dengan binding dua arah via model() import { Component, model, output, computed } from '@angular/core'; @Component({ selector: 'app-search-input', standalone: true, template: `
@if (query().length > 0) { } {{ charCount() }} characters
` }) export class SearchInputComponent { // model() membuat Signal dua arah // Modifikasi dipropagasikan ke parent query = model(''); // Input klasik untuk konfigurasi placeholder = model('Search...'); // Output untuk event tambahan searchSubmitted = output(); // Computed berdasarkan model charCount = computed(() => this.query().length); onInput(event: Event) { const value = (event.target as HTMLInputElement).value; // Update model - dipropagasikan ke parent this.query.set(value); } clear() { this.query.set(''); } submit() { if (this.query().length > 0) { this.searchSubmitted.emit(this.query()); } } } ``` Parent menggunakan sintaks banana-in-a-box `[()]` untuk binding dua arah: ```typescript // app.component.ts // Penggunaan two-way binding dengan model() import { Component, signal, effect } from '@angular/core'; import { SearchInputComponent } from './search-input.component'; @Component({ selector: 'app-root', standalone: true, imports: [SearchInputComponent], template: `

Current search: {{ searchTerm() }}

@for (result of filteredResults(); track result.id) {
{{ result.name }}
}
` }) export class AppComponent { // Signal lokal yang disinkronkan dengan komponen child searchTerm = signal(''); results = signal([ { id: 1, name: 'Angular 18' }, { id: 2, name: 'React 19' }, { id: 3, name: 'Vue 3' } ]); // Filtering reaktif berdasarkan searchTerm filteredResults = computed(() => { const term = this.searchTerm().toLowerCase(); if (!term) return this.results(); return this.results().filter(r => r.name.toLowerCase().includes(term) ); }); } ``` > **model() vs input()** > > Gunakan `input()` untuk data hanya-baca (parent ke child). Gunakan `model()` saat komponen child perlu memodifikasi nilai (dua arah). ## Signal Queries dengan viewChild() dan contentChild() Fungsi `viewChild()`, `viewChildren()`, `contentChild()`, dan `contentChildren()` menggantikan dekorator yang sesuai. Fungsi-fungsi ini mengembalikan Signals, menghilangkan kebutuhan lifecycle hooks seperti `ngAfterViewInit`. ```typescript // form-container.component.ts // Demonstrasi signal queries import { Component, viewChild, viewChildren, ElementRef, effect, signal } from '@angular/core'; import { FormFieldComponent } from './form-field.component'; @Component({ selector: 'app-form-container', standalone: true, imports: [FormFieldComponent], template: `
` }) export class FormContainerComponent { // viewChild mengembalikan Signal formElement = viewChild('formElement'); // viewChild.required menjamin elemen tersedia firstInput = viewChild.required>('firstInput'); // Query pada komponen - mengembalikan komponen itu sendiri firstFormField = viewChild(FormFieldComponent); // viewChildren untuk beberapa elemen allFormFields = viewChildren(FormFieldComponent); constructor() { // Effect menggantikan ngAfterViewInit untuk queries effect(() => { // Signal secara otomatis di-resolve const input = this.firstInput(); console.log('First input available:', input.nativeElement); }); // Bereaksi terhadap perubahan list effect(() => { const fields = this.allFormFields(); console.log(`${fields.length} form fields found`); }); } focusFirst() { // Akses langsung via Signal this.firstInput().nativeElement.focus(); } onSubmit(event: Event) { event.preventDefault(); // Akses formulir const form = this.formElement(); if (form) { console.log('Form submitted'); } } } ``` Untuk memproyeksikan konten dan mengaksesnya, `contentChild()` bekerja dengan cara serupa: ```typescript // card.component.ts // Menggunakan contentChild untuk konten yang diproyeksikan import { Component, contentChild, contentChildren, TemplateRef } from '@angular/core'; @Component({ selector: 'app-card', standalone: true, template: `
@if (hasFooter()) {
}
` }) export class CardComponent { // Mendeteksi apakah footer diproyeksikan footerContent = contentChild('[card-footer]'); // Computed untuk memeriksa keberadaan footer hasFooter = computed(() => this.footerContent() !== undefined); } ``` ## Deteksi Perubahan Zoneless Angular 18 memperkenalkan deteksi perubahan tanpa Zone.js dalam mode eksperimental. Fitur ini mengurangi ukuran bundle sekitar 13 KB dan meningkatkan performa dengan menghilangkan monkey-patch pada API asinkron browser. ```typescript // main.ts // Mengkonfigurasi aplikasi dalam mode zoneless import { bootstrapApplication } from '@angular/platform-browser'; import { provideExperimentalZonelessChangeDetection } from '@angular/core'; import { AppComponent } from './app/app.component'; bootstrapApplication(AppComponent, { providers: [ // Mengaktifkan deteksi zoneless eksperimental provideExperimentalZonelessChangeDetection() ] }); ``` Konfigurasi `angular.json` juga perlu diperbarui untuk menghapus Zone.js: ```json { "projects": { "my-app": { "architect": { "build": { "options": { "polyfills": [] } } } } } } ``` Dalam mode zoneless, deteksi perubahan dipicu secara otomatis dalam kasus berikut: pembaruan Signal, pemanggilan `markForCheck()`, nilai baru diterima melalui AsyncPipe, atau attach/detach komponen. ```typescript // zoneless-counter.component.ts // Komponen yang dioptimalkan untuk mode zoneless import { Component, signal, ChangeDetectionStrategy, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { toSignal } from '@angular/core/rxjs-interop'; @Component({ selector: 'app-zoneless-counter', standalone: true, // OnPush direkomendasikan untuk zoneless changeDetection: ChangeDetectionStrategy.OnPush, template: `

Counter: {{ count() }}

@if (loading()) {

Loading...

} @if (data()) {
{{ data() | json }}
}
` }) export class ZonelessCounterComponent { private http = inject(HttpClient); count = signal(0); loading = signal(false); data = signal(null); increment() { // Pembaruan Signal memicu deteksi this.count.update(c => c + 1); } async fetchData() { this.loading.set(true); try { // Signals menjamin pembaruan tampilan const response = await fetch('/api/data'); const json = await response.json(); this.data.set(json); } finally { this.loading.set(false); } } } ``` > **Kompatibilitas Zoneless** > > Komponen yang menggunakan `ChangeDetectionStrategy.OnPush` dan Signals umumnya kompatibel dengan mode zoneless. Hindari modifikasi langsung pada properti yang bukan Signal. ## Migrasi Komponen yang Ada Migrasi ke API berbasis signal dapat dilakukan secara bertahap. Berikut contoh refactoring komponen tradisional: ```typescript // SEBELUM: Komponen dengan dekorator klasik // user-profile-legacy.component.ts import { Component, Input, ViewChild, ElementRef, AfterViewInit } from '@angular/core'; @Component({ selector: 'app-user-profile-legacy', template: `

{{ user.name }}

{{ user.email }}

` }) export class UserProfileLegacyComponent implements AfterViewInit { @Input() user!: { name: string; email: string }; @ViewChild('container') container!: ElementRef; ngAfterViewInit() { console.log('Container ready:', this.container.nativeElement); } } ``` ```typescript // SESUDAH: Komponen yang dimigrasikan ke Signals // user-profile.component.ts import { Component, input, viewChild, ElementRef, effect } from '@angular/core'; interface User { name: string; email: string; } @Component({ selector: 'app-user-profile', standalone: true, template: `

{{ user().name }}

{{ user().email }}

` }) export class UserProfileComponent { // input.required menggantikan @Input() dengan ! user = input.required(); // viewChild.required menggantikan @ViewChild dengan ! container = viewChild.required('container'); constructor() { // effect menggantikan ngAfterViewInit untuk queries effect(() => { console.log('Container ready:', this.container().nativeElement); }); } } ``` Manfaat migrasi ini: typing yang lebih ketat, reaktivitas otomatis, kode boilerplate yang lebih sedikit, dan kompatibilitas dengan mode zoneless. ## Praktik Terbaik dengan Signals Berikut rekomendasi utama untuk memaksimalkan penggunaan Signals di Angular 18: ```typescript // best-practices.component.ts // Contoh praktik terbaik dengan Signals import { Component, signal, computed, effect, untracked, ChangeDetectionStrategy } from '@angular/core'; interface Product { id: string; name: string; price: number; quantity: number; } @Component({ selector: 'app-cart', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `

Cart ({{ itemCount() }} items)

@for (item of items(); track item.id) {
{{ item.name }} {{ item.quantity }} × \${{ item.price }}
}
Total: \${{ total() }}
` }) export class CartComponent { // Signal untuk data yang mutable items = signal([]); // Computed untuk nilai turunan - menghindari perhitungan ulang yang tidak perlu itemCount = computed(() => this.items().length); total = computed(() => this.items().reduce((sum, item) => sum + item.price * item.quantity, 0) ); constructor() { // Effect untuk side effects (analytics, persistence) effect(() => { const currentItems = this.items(); // untracked menghindari pembuatan dependensi untracked(() => { localStorage.setItem('cart', JSON.stringify(currentItems)); }); }); } addItem(product: Product) { // update() untuk modifikasi berdasarkan state sebelumnya this.items.update(current => { const existing = current.find(i => i.id === product.id); if (existing) { return current.map(i => i.id === product.id ? { ...i, quantity: i.quantity + 1 } : i ); } return [...current, { ...product, quantity: 1 }]; }); } removeItem(id: string) { this.items.update(current => current.filter(i => i.id !== id)); } } ``` Poin-poin utama yang perlu diingat: - Gunakan `computed()` untuk nilai turunan daripada menghitung ulang di template - Pilih `update()` daripada `set()` saat nilai baru bergantung pada nilai lama - Gunakan `untracked()` dalam effects untuk menghindari dependensi melingkar - Selalu tentukan `track` dalam loop `@for` untuk mengoptimalkan rendering ## Kesimpulan Angular 18 meletakkan fondasi untuk masa depan tanpa Zone.js melalui Signals. Poin-poin utama: - **input()** menggantikan `@Input()` dengan typing yang lebih ketat dan akses hanya-baca yang terjamin - **model()** memungkinkan two-way binding reaktif antara parent dan child - **viewChild()** dan **contentChild()** menghilangkan kebutuhan lifecycle hooks - **Zoneless** mengurangi ukuran bundle dan meningkatkan performa - **computed()** dan **effect()** melengkapi ekosistem reaktif - Migrasi bertahap dapat dilakukan komponen per komponen Mengadopsi Signals mempersiapkan aplikasi Angular untuk versi mendatang di mana mode zoneless akan menjadi standar. Transisi ini merupakan investasi yang bijak untuk maintainability dan performa jangka panjang. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/id/blog/angular/angular-18-signals-new-features