Service adalah class biasa yang menyimpan data atau logic yang dipakai bareng oleh banyak komponen. Dependency injection (DI) adalah cara Angular mengantarkan service itu ke komponen yang memintanya. Bayangin warung Martabak Bang Jali punya satu buku pesanan di meja kasir. Yang bikin martabak dan yang menerima pesanan gak bawa buku sendiri-sendiri; mereka minta buku yang sama ke Bang Jali. Buku itu service-nya, Bang Jali yang membagikannya itu injector, dan "minta buku" itu inject().
🧪 Tebakan cepat: MenuItem dan Pesanan masing-masing menulis
keranjang = new KeranjangService(). Kamu klik Pesan di kartu dua kali. Pesanan menampilkan berapa item? Jawabannya ada di bagian "Satu instance untuk semua".
Service: class dengan @Injectable
// src/app/keranjang.service.ts
import { Injectable, signal } from '@angular/core'
@Injectable({ providedIn: 'root' })
export class KeranjangService {
isi = signal<string[]>([])
tambah(nama: string) {
this.isi.update((isi) => [...isi, nama])
}
}
@Injectable()menandai class ini sebagai service yang bisa disuntikkan (di-inject) Angular ke komponen.providedIn: 'root'mendaftarkannya di root injector: satu instance untuk seluruh aplikasi, dibuat saat pertama kali diminta. Gak perlu didaftarkan di tempat lain lagi.- Sisanya class biasa: properti
isidan methodtambah(), tanpa template. isisebuah signal, dantambah()membuat array baru denganupdate. Sejak Angular 22 komponen secara bawaan OnPush: komponen cuma digambar ulang kalau ada yang memberi kabar, misalnya signal yang dibaca template-nya berubah. Dengan array biasa danpush, Pesanan gak pernah dapat kabar dan tetap menampilkan 0 item.
inject(): meminta service
// src/app/menu-item/menu-item.ts
import { Component, Input, inject } from '@angular/core'
import { KeranjangService } from '../keranjang.service'
@Component({
selector: 'app-menu-item',
standalone: true,
template: `
<h3>{{ nama }}</h3>
<button (click)="pesan()">Pesan</button>
`,
})
export class MenuItem {
@Input() nama = ''
private keranjang = inject(KeranjangService)
pesan() {
this.keranjang.tambah(this.nama)
}
}
// src/app/pesanan/pesanan.ts
import { Component, inject } from '@angular/core'
import { KeranjangService } from '../keranjang.service'
@Component({
selector: 'app-pesanan',
standalone: true,
template: `<p>Keranjang: {{ keranjang.isi().length }} item</p>`,
})
export class Pesanan {
keranjang = inject(KeranjangService)
}
inject(KeranjangService)meminta service ke Angular. Tulis di properti seperti di atas, atau di dalamconstructor. Angular yang membuatkan instance-nya; kamu gak menulisnew.- Bentuk lama yang masih sering ketemu, hasilnya sama:
constructor(private keranjang: KeranjangService) {}. - MenuItem dan Pesanan gak saling kenal. Template App cukup
<app-menu-item nama="Martabak Telur Spesial" />dan<app-pesanan />; keduanya ngobrol lewat service.
| Kejadian | keranjang.isi() |
Pesanan tampil |
|---|---|---|
| Render pertama | [] |
Keranjang: 0 item |
| Klik Pesan di kartu Martabak Telur Spesial | ['Martabak Telur Spesial'] |
Keranjang: 1 item |
| Klik Pesan sekali lagi | dua nama | Keranjang: 2 item |
Latihan memakai pola yang sama: DataService bertanda @Injectable({ providedIn: 'root' }) punya getMessage() yang mengembalikan 'Data dari service!', komponennya menulis private data = inject(DataService) lalu message = this.data.getMessage(), dan template-nya <p>{{ message }}</p>.
Satu instance untuk semua
Dengan new, tiap komponen bikin keranjang sendiri. MenuItem menambah ke keranjang miliknya, Pesanan membaca keranjang miliknya yang masih kosong. Jadi sesudah dua klik, Pesanan tetap Keranjang: 0 item. Itu jawaban tebakannya.
Dengan inject() dan providedIn: 'root', Angular memberi instance yang sama ke semua yang meminta:
| Cara mendapat keranjang | Jumlah instance | Pesanan sesudah 2 klik |
|---|---|---|
new KeranjangService() di tiap komponen |
satu per komponen | Keranjang: 0 item |
inject(KeranjangService) + providedIn: 'root' |
satu untuk semua | Keranjang: 2 item |
Halaman yang memakai data yang sama (keranjang, akun yang login, data dari server) jadi selalu sinkron.
providers di komponen: instance sendiri
Kadang kamu memang ingin instance terpisah. Daftarkan service di providers milik komponen:
@Component({
selector: 'app-menu-item',
standalone: true,
providers: [KeranjangService],
template: `...`,
})
export class MenuItem { /* sama seperti tadi */ }
Angular mencari service dari komponen itu ke atas: providers miliknya dulu, lalu induknya, terakhir root injector. Dengan baris ini tiap kartu MenuItem dapat keranjang baru. Klik dua kali di kartu pertama dan sekali di kartu kedua: kartu pertama memegang 2 item, kartu kedua 1 item, dan Pesanan (yang gak punya providers) memegang instance root yang tetap 0.
Kalau service-nya gak ketemu
Tanpa providedIn dan tanpa providers di mana pun, Angular gak punya resep untuk membuatnya. Komponen yang memintanya gagal dibuat, dan console menulis:
ERROR NG0201: No provider found for `KeranjangService`. Source: Environment Injector. Find more at https://v22.angular.dev/errors/NG0201
Hasilnya sama kalau class-nya lupa diberi @Injectable() sama sekali. Angular 18 menulis pesan yang sama dengan nama lain: NullInjectorError: No provider for KeranjangService!. Perbaikannya: tambahkan { providedIn: 'root' }, atau daftarkan di providers.
inject() juga cuma boleh dipanggil saat Angular sedang membuat sesuatu: di properti class atau di constructor. Di dalam method yang jalan karena klik, hasilnya error:
NG0203: The `KeranjangService` token injection failed. `inject()` function must be called from an injection context such as a constructor, a factory function, a field initializer, or a function used with `runInInjectionContext`. Find more at https://v22.angular.dev/errors/NG0203
Kenapa minta, bukan bikin sendiri
Komponen yang memakai inject() gak peduli dari mana keranjangnya datang. Di test, kamu bisa menukarnya dengan keranjang palsu tanpa mengubah satu baris pun di Pesanan:
TestBed.configureTestingModule({
imports: [Pesanan],
providers: [{ provide: KeranjangService, useValue: { isi: signal(['Martabak Telur Spesial']), tambah() {} } }],
})
Pesanan di test ini menampilkan Keranjang: 1 item. Itu keuntungan utama DI: testability (gampang dites), plus loose coupling, karena komponen gak terikat ke cara service dibuat.
⚠️ Jebakan yang sering ketemu
newdi tiap komponen. Tiapnewmembuat salinan sendiri, jadi data gak pernah sampai ke komponen lain. Minta lewatinject().- Lupa
providedIn: 'root'.@Injectable()saja belum mendaftarkan apa pun:NG0201: No provider found for KeranjangService(di Angular 18:NullInjectorError: No provider for KeranjangService!). - Data bersama di properti biasa.
isi: string[] = []denganpushgak memberi kabar, jadi komponen lain (OnPush sejak Angular 22) tetap menampilkan isi lama. Simpan di signal. providers: [KeranjangService]di komponen tanpa sadar. Komponen itu dan anak-anaknya dapat instance baru yang terpisah dari root.- Detail latihan. Penilai mencari pemanggilan seperti
= this.data.getMessage(, jadi simpan service-nya di properti dulu, baru panggil method-nya.inject(DataService).getMessage()dalam satu baris, atau teksData dari service!yang diketik di template, gak diterima.
TL;DR: Service adalah class bertanda @Injectable({ providedIn: 'root' }) yang menyimpan data atau logic untuk banyak komponen. Komponen memintanya dengan inject(KeranjangService) (atau lewat constructor), bukan new, dan dengan providedIn: 'root' semua yang meminta dapat satu instance yang sama, jadi Pesan di MenuItem langsung terbaca di Pesanan. providers: [KeranjangService] di komponen memberi instance sendiri. Data yang dipakai bersama disimpan di signal, supaya komponen lain ikut diperbarui. Service yang gak terdaftar memicu NG0201 (No provider found for ...; di Angular 18 NullInjectorError), dan inject() di dalam method memicu NG0203. Karena komponen cuma meminta, service-nya bisa ditukar dengan versi palsu di test: itu testability.