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: `
Time: {{ showCurrentTime() }}
Toggle Grandchild's background
`,
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: `
Child Component
Time: {{ showCurrentTime() }}
`,
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: `
Grandchild Component
{{ showCurrentTime() }}
toggle: {{ toggleGrandchild() }}
`,
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
{{ `Time: ${showCurrentTime()}` }}
(click)="toggle()">Toggle background
class="child" >
/>
/>
@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: `
Child Component
Time: {{ showCurrentTime() }}
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPushChildComponent {}
The OnPushChildComponent
component does not change to add the toggleGrandchild
input.
@Component({
selector: 'app-on-push-grandchild',
template: `
Grandchild Component
Time: {{ showCurrentTime() }}
`,
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#