Prop Drilling
in Angular occurs when an input passes through intermediate components from the root component to a descendant component. It is considered an anti-pattern because it leads to tight coupling and unmaintainable codes and impacts performance by triggering unnecessary change detection cycles.
The original demo displays a component tree where the App
component has two child components. Each child component has a grandchild component. The App displays a button that toggles the background color of the grandchild component. The value is passed from the App component to the grandchild component through the child component. Finally, the grandchild component uses the value to switch the background color between yellow and transparent.
The revised solution uses the provide/inject pattern to fix the prop drilling anti-pattern. The App
component declares an InjectionToken to provide the toggle value. Next, the grandchild component injects the token to obtain the toggle value. Finally, the grandchild component uses the toggle value to determine its background color.
Prop Drilling in the Angular Components
Demo 1: Prop Drilling Solution
@Component({
selector: 'app-root',
imports: [OnPushChildComponent],
template: `
<p>Time: {{ showCurrentTime() }}</p>
<button (click)="toggle()">Toggle Grandchild's background</button>
<div class="child" >
<app-on-push-child [toggleGrandchild]="toggleGrandchild()" />
<app-on-push-child [toggleGrandchild]="toggleGrandchild()" />
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
toggleGrandchild = signal(false);
toggle() {
this.toggleGrandchild.update((prev) => !prev);
}
showCurrentTime() {
return getCurrentTime();
}
}
The App
component has a button that toggles the value of the toggleGrandchild
signal. The toggleGrandchild
signal is passed to the OnPushChildComponent
component. Moreover, the showCurrentTime
method displays when a change detection cycle occurs. Button click triggers an event; therefore, the App
component updates the time in the change detection cycle.
@Component({
selector: 'app-on-push-child',
imports: [OnPushGrandChildComponent],
template: `
<div class="container">
<h3>Child Component</h3>
<p>Time: {{ showCurrentTime() }}</p>
<app-on-push-grandchild [toggleGrandchild]="toggleGrandchild()" />
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPushChildComponent {
toggleGrandchild = input(false);
showCurrentTime() {
return getCurrentTime();
}
}
The OnPushChildComponent
component does not use the toggleGrandchild
signal input and simply passes it to the OnPushGrandchildComponent
component. However, the OnPushChildComponent
component receives a new input, and the change detection cycle updates the template to show the new current time.
@Component({
selector: 'app-on-push-grandchild',
template: `
<div class="container" [style.background]="background()">
<h3>Grandchild Component</h3>
<p>{{ showCurrentTime() }}</p>
<p>toggle: {{ toggleGrandchild() }}</p>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPushGrandchildComponent {
toggleGrandchild = input(false);
background = computed(() => this.toggleGrandchild() ? 'yellow' : 'transparent');
showCurrentTime() {
return getCurrentTime();
}
}
The OnPushGrandchildComponent
receives the toggleGrandchild
signal input, and the background
computed signal uses the input to decide the background color. In the template, [style.background]
changes the background CSS style based on the value of the background
computed signal
This approach leads to several problems:
- Unmaintainable codes: When the App component has to pass more inputs to the OnPushGrandchildComponent, the OnPushChildComponent component will also be updated.
- Extra change detection cycle: the OnPushChildComponent is updated when its inputs receive new values even though the component does not use them
- Tight coupling: the OnPushChildComponent component is not reusable when other components are unable to provide toggleGrandchild input to it
Stackblitz demo: https://stackblitz.com/edit/stackblitz-starters-jcbfxw5a?file=src%2Fmain.ts
Next, I will show how to avoid prop drilling by using InjectionToken and provide/inject pattern
Provide/Inject Pattern
Demo 2: Provide/Inject in Providers Array
Create InjectionTokens for Toggling
import { InjectionToken, Signal, WritableSignal } from "@angular/core";
export const TOGGLE_TOKEN = new InjectionToken<WritableSignal<boolean>>('TOGGLE_TOKEN');
export const BACKGROUND_TOKEN = new InjectionToken<Signal<string>>('BACKGROUND_TOKEN');
Declare TOGGLE_TOKEN and BACKGROUND_TOKEN tokens to inject the toggle value and background color.
import { computed, signal } from '@angular/core';
import { BACKGROUND_TOKEN, CURRENT_TIME_TOKEN, TOGGLE_TOKEN } from './toggle.constant';
export const toggleValue = signal(false);
export const background = computed(() => toggleValue()? 'yellow' : 'transparent');
export const toggleProviders = [
{
provide: TOGGLE_TOKEN,
useValue: toggleValue,
},
{
provide: BACKGROUND_TOKEN,
useValue: background,
},
]
The toggleProviders
array provides the values of the InjectionToken. The TOGGLE_TOKEN
token provides a boolean signal and the BACKGROUND_TOKEN
token provides a computed signal that returns the background color.
Providers Array in the App Component
<p>{{ `Time: ${showCurrentTime()}` }}</p>
<button (click)="toggle()">Toggle background</button>
<div class="child" >
<app-on-push-child />
<app-on-push-child />
</div>
@Component({
selector: 'app-root',
imports: [OnPushChildComponent],
templateUrl: './app.component.html',
providers: toggleProviders,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
toggleValue = inject(TOGGLE_TOKEN);
showCurrentTime = inject(CURRENT_TIME_TOKEN);
toggle() {
this.toggleValue.update((prev) => !prev);
}
}
The App
component injects the TOGGLE_TOKEN
to obtain the boolean signal. When the button is clicked in the HTML template, the value of the toggleValue
is toggled.
Child Component
@Component({
selector: 'app-on-push-child',
imports: [OnPushGrandChildComponent],
template: `
<h3>Child Component</h3>
<p>Time: {{ showCurrentTime() }}</p>
<app-on-push-grand-child />
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPushChildComponent {}
The OnPushChildComponent
component does not change to add the toggleGrandchild
input.
@Component({
selector: 'app-on-push-grandchild',
template: `
<div class="container" [style.background]="background()">
<h3>Grandchild Component</h3>
<p>Time: {{ showCurrentTime() }}</p>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPushGrandchildComponent {
background = inject(BACKGROUND_TOKEN)
}
The OnPushGrandchildComponent
component injects the BACKGROUND TOKEN
to obtain the computed signal. The background
computed signal is assigned to [style.background]
to change the CSS background color.
When users click the button in the App
component, the background color of the OnPushGrandchildComponent
is switched between yellow and transparent. Moreover, the template also updates the current time. However, the OnPushChildComponent
is unaffected and the timestamp is not updated.
Using the Provide/Inject pattern has the following benefits:
- Maintainable codes: The intermediate components are not modified to support the new signal inputs
- Reusability: The intermediate components are reusable because they don’t need the toggleValue signal input
- Performance: The intermediate components are not updated by the change detection cycles.
Stackblitz demo: https://stackblitz.com/edit/stackblitz-starters-2jjtyjzq?file=src%2Fmain.ts
References:
- InjectionToken: https://angular.dev/guide/di/lightweight-injection-tokens#using-lightweight-injection-tokens
- Inject function: https://angular.dev/api/core/inject#
Top comments (4)
Great article, thank you
what are the advantages over a signal in a service?
Signal in a service in another solution that I did not post. This is because most Angular architects already know about it.
Services are in the root injector when created with {providedIn: root }. The services can be injected into any component, making their signals global.
If the signals are global, then any component in the App's component tree can access them, which may or may not be your intention. Suppose the data is not meant to be global. In that case, you have to remove { providedIn: root } from the Injectable decorator, and provide the service in the providers array of the component.
When we provide the InjectionToken in the component, the data is available in the components and its descendants, limiting the data scope to fewer components. Moreover, we can use the same InjectionToken to provide different values in its descendants, allowing them to change their behavior.
In the end, it depends on your use case. InjectionToken + provide/inject is lightweight and suitable for small to medium-sized applications. You may not need to use state management library for applications with few stateful properties.
yes ... "events" to the rescue ALSO ... this could blow in your face ..
for example:
now when triggering event both are triggered and both do something !
Be careful when using it like this, cause it can cause some pretty serious issues.
It's a good idea which must be carefully though of ;)
It's good practice to manage those kind of events in component scope, not globally.
Or globally but then you really really have to know what you're doing ...