State Management Angular — Angular

Mengelola state di Angular bisa dengan Signals, Services, atau NgRx. Signal-based Store @Injectable({ providedIn: "root" }) export class CartStore { private…

State adalah data yang sedang dipegang aplikasi saat ini: isi keranjang, siapa yang login, filter menu yang dipilih. State management adalah keputusan di mana data itu disimpan dan siapa yang boleh mengubahnya, supaya semua bagian halaman menunjukkan angka yang sama. Bayangin papan pesanan di dinding warung Martabak Bang Jali. Kasir, dapur, dan kurir semuanya membaca papan yang sama, dan cuma Bang Jali yang menulis di papan itu. Total di struk dihitung dari papan, bukan dicatat terpisah. Kalau kurir menyimpan hitungan sendiri di buku sakunya, cepat atau lambat hitungannya beda dengan papan.

🧪 Tebakan cepat: KeranjangStore menyimpan daftar isi, tapi juga jumlah = signal(0) yang ditambah satu di tambah(). buang() cuma menghapus dari isi. Pembeli memesan 2 martabak, lalu membuang 1. Header, yang membaca jumlah(), menampilkan berapa item? Jawabannya ada di bagian "Satu sumber kebenaran".

State lokal dan state bersama

Signal store: service yang menjaga state

import { Injectable, computed, signal } from '@angular/core'

export interface Item {
  nama: string
  harga: number
}

@Injectable({ providedIn: 'root' })
export class KeranjangStore {
  private isi = signal<Item[]>([])

  readonly daftar = this.isi.asReadonly()
  readonly jumlah = computed(() => this.isi().length)
  readonly total = computed(() => {
    console.log('total dihitung ulang')
    return this.isi().reduce((t, item) => t + item.harga, 0)
  })

  tambah(item: Item) {
    this.isi.update((isi) => [...isi, item])
  }

  buang(index: number) {
    this.isi.update((isi) => isi.filter((_, i) => i !== index))
  }

  kosongkan() {
    this.isi.set([])
  }
}

Dipakai di komponen

@Component({
  selector: 'app-header',
  standalone: true,
  template: `<p class="badge">Keranjang: {{ store.jumlah() }} item, Rp{{ store.total() }}</p>`,
})
export class Header {
  store = inject(KeranjangStore)
}

// template MenuPage:       <button (click)="store.tambah(m)">Pesan</button>
// template KeranjangPanel: <button (click)="store.buang($index)">Buang</button>
Kejadian daftar() Header Console
Aplikasi dibuka [] Keranjang: 0 item, Rp0 total dihitung ulang
Pesan Martabak Manis Cokelat Keju (40000) 1 item Keranjang: 1 item, Rp40000 total dihitung ulang
Pesan Martabak Telur Spesial (50000) 2 item Keranjang: 2 item, Rp90000 total dihitung ulang
Buang item pertama 1 item Keranjang: 1 item, Rp50000 total dihitung ulang

MenuPage gak pernah memberi tahu Header. Yang berubah cuma isi di store, dan computed menghitung ulang total sekali tiap kali isi berubah, lalu memakai hasilnya untuk semua yang membaca.

Satu sumber kebenaran

Versi tebakan tadi menyimpan nilai turunan sendiri:

readonly jumlah = signal(0)
readonly total = signal(0)

tambah(item: Item) {
  this.isi.update((isi) => [...isi, item])
  this.jumlah.update((n) => n + 1)
  this.total.update((t) => t + item.harga)
}

buang(index: number) {
  this.isi.update((isi) => isi.filter((_, i) => i !== index))
  // lupa memperbarui jumlah dan total
}
Kejadian daftar() Header
Pesan 2 martabak 2 item Keranjang: 2 item, Rp90000
Buang 1 1 item Keranjang: 2 item, Rp90000

Header tetap 2 item padahal keranjangnya tinggal satu. Itu jawaban tebakannya. Ada dua sumber kebenaran, isi dan jumlah, dan setiap method harus ingat memperbarui keduanya. Begitu satu lupa, angkanya beda. Dengan computed, gak ada yang perlu diingat: jumlah dan total selalu dihitung dari isi.

asReadonly: yang boleh mengubah cuma store

Kalau KeranjangPanel menulis this.store.daftar.set([]), proyek CLI gagal dikompilasi:

Property 'set' does not exist on type 'Signal<Item[]>'.

Komponen cuma bisa meminta perubahan lewat method store, misalnya this.store.kosongkan(). Jadi kalau keranjang tiba-tiba kosong, cukup cari siapa yang memanggil kosongkan().

Kapan NgRx

Signals adalah fitur bawaan Angular (sejak versi 16, stabil sejak 17), dan service dengan signal seperti KeranjangStore cukup untuk kebanyakan aplikasi. NgRx adalah library state management populer untuk Angular yang terinspirasi Redux: semua state ada di satu Store, diubah cuma lewat action yang diproses reducer, dan dibaca lewat selector.

// gaya NgRx (library terpisah, gak ada di Playground)
export const tambah = createAction('[Menu] Tambah', props<{ item: Item }>())

export const keranjangReducer = createReducer(
  { isi: [] as Item[] },
  on(tambah, (state, { item }) => ({ ...state, isi: [...state.isi, item] })),
)

// di komponen: this.store.dispatch(tambah({ item: m }))
Kebutuhan Pilihan
dipakai satu komponen signal di komponen itu
dibagi beberapa komponen signal store seperti KeranjangStore
alur asinkron rumit (debounce, membatalkan request lama) service + RxJS
state kompleks yang dibagi banyak komponen, tim besar, perlu jejak setiap perubahan di DevTools NgRx

Mulai dari signal. Naik ke NgRx kalau manfaatnya jelas, karena tiap perubahan butuh action, reducer, dan selector sendiri.

⚠️ Jebakan yang sering ketemu

TL;DR: State bersama, seperti keranjang yang dibaca Header, MenuPage, dan KeranjangPanel, disimpan di satu service providedIn: 'root': sebuah signal store. Simpan datanya di private isi = signal(...), buka asReadonly() untuk dibaca, turunkan jumlah dan total dengan computed supaya ada satu sumber kebenaran, dan ubah lewat method yang membuat array baru. Nilai turunan yang disimpan terpisah pasti tertinggal. Signals adalah fitur bawaan Angular yang cukup untuk kebanyakan state; NgRx, library bergaya Redux, dipakai untuk state kompleks yang dibagi banyak komponen di aplikasi besar.

Yang akan kamu pelajari