Angular deliberately has no built-in "global variable" mechanism — component classes are scoped instances, and reaching for a literal module-level let to hold shared state works but bypasses change detection, so the UI won't reliably update when the value changes. There are two real, framework-supported patterns instead, and which one fits depends on how far apart the components are.
1. Parent and direct child: @Input and @Output
// parent.ts
<app-child [count]="count" (increment)="count = count + 1"></app-child>
// child.ts
@Input() count = 0;
@Output() increment = new EventEmitter<void>();
This is the correct tool when the relationship is a direct parent-child pair — it's explicit, type-checked, and easy to trace in the template. It stops being practical once state needs to reach a grandchild three levels down, or a sibling component with no direct relationship at all — that would mean threading the same data through every intermediate component purely to pass it along, whether or not that component itself needs it.
2. Unrelated components: a shared service
@Injectable({ providedIn: 'root' })
export class CartService {
private itemCount = 0;
get count(): number {
return this.itemCount;
}
add(): void {
this.itemCount++;
}
}
Because providedIn: 'root' makes the service a genuine singleton, any two components anywhere in the app — a product card and a completely unrelated header badge — that inject CartService are reading and writing the exact same instance. This is the standard replacement for what other frameworks might call a "global": scoped to the app's lifetime, but still a real injected dependency rather than a bare module-level variable.
3. Making the shared state reactive
A plain property on a service updates correctly, but nothing tells a component to re-render when it changes unless that component happens to re-run change detection for an unrelated reason. Expose the value as an Observable (or, in modern Angular, a signal) so components can subscribe and react automatically:
@Injectable({ providedIn: 'root' })
export class CartService {
private itemCountSubject = new BehaviorSubject<number>(0);
itemCount$ = this.itemCountSubject.asObservable();
add(): void {
this.itemCountSubject.next(this.itemCountSubject.value + 1);
}
}
<span>{{ cartService.itemCount$ | async }}</span>
With the async pipe subscribed in the template, every component displaying itemCount$ updates the moment any other component calls add() — no manual event wiring between the two components is needed at all.
4. Signals as the newer alternative
@Injectable({ providedIn: 'root' })
export class CartService {
itemCount = signal(0);
add(): void {
this.itemCount.update((count) => count + 1);
}
}
<span>{{ cartService.itemCount() }}</span>
Signals (Angular 16+) achieve the same shared-reactive-state result with less boilerplate than a BehaviorSubject — no explicit subscription management, and the template just calls the signal as a function to read its current value.