When You Shouldn’t Use Input in Angular

George Hulpoi3 min read · Jan 4

In Angular, Inputs are a fundamental feature used to pass data to components, influencing their appearance or behavior. However, there are scenarios where Inputs might not be the best approach. This article explores alternative strategies to replace Inputs when necessary.

Summary

Sometimes, child components require access to the parent component's instance. A common but flawed approach is to pass the parent component as an Input. This can lead to cumbersome and inefficient code, especially with deeply nested components.

1234567891011121314151617181920212223242526272829303132333435
import { Component, input, effect } from '@angular/core';

@Component({
  selector: 'app-child',
  template: `#child`,
  standalone: true,
})
export class ChildComponent {
  parent = input<ParentComponent>();

  constructor() {
    effect(() => {
      console.log(`Parent.someProp: ${this.parent()?.someProp} (should be 5)`);
    });
  }
}

@Component({
  selector: 'app-parent',
  template: `
    <app-child [parent]="instance" /> 
    <app-child [parent]="instance" /> 
    <app-child [parent]="instance" />
  `,
  standalone: true,
  imports: [ChildComponent],
})
export class ParentComponent {
  instance?: ParentComponent;
  someProp = 5;

  constructor() {
    this.instance = this;
  }
}

Instead, a more efficient method is to use Angular's Dependency Injection (DI) system to provide the component itself.

123456789101112131415161718192021222324252627282930313233343536373839
import { Component, inject } from '@angular/core';
import { InjectionToken } from '@angular/core';

const PARENT_TOKEN = new InjectionToken<ParentComponent>(
  'ParentComponentInstance'
);

@Component({
  selector: 'app-child',
  template: `#child`,
  standalone: true,
})
export class ChildComponent {
  parent = inject(PARENT_TOKEN, { optional: true });

  constructor() {
    console.log(`Parent.someProp: ${this.parent?.someProp} (should be 5)`);
  }
}

@Component({
  selector: 'app-parent',
  template: `
    <app-child /> 
    <app-child /> 
    <app-child />
  `,
  standalone: true,
  imports: [ChildComponent],
  providers: [
    {
      provide: PARENT_TOKEN,
      useExisting: ParentComponent,
    },
  ],
})
export class ParentComponent {
  someProp = 5;
}

By leveraging DI, you can inject the parent component directly into the child, simplifying the code and improving maintainability. This technique is commonly used in Angular Material components, such as MatGridList.

Input drilling occurs when data is passed through multiple layers of components, only to be used by a deeply nested child component. This practice can violate the Single-responsibility principle, as intermediate components act merely as proxies without any need for the data. Below is the source code for an example demonstrating Input Drilling.

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
import { Component, input, computed, booleanAttribute } from '@angular/core';

@Component({
  selector: 'app-item',
  template: `
  <article>
    <h1>{{ title() }}</h1>
    @if (!hideDescription) {
      <p>{{ description() }}</p>
    }
  </article>
  `,
  standalone: true,
})
export class ItemComponent {
  data = input.required<IArticle>();
  title = computed(() => this.data().title);
  description = computed(() => this.data().description);
  hideDescription = input(false, { transform: booleanAttribute });
}

@Component({
  selector: 'app-page-list',
  template: `
  @for (item of items; track item.id) {
    <app-item [data]="item" [hideDescription]="hideItemsDescription" />
  }
  `,
  standalone: true,
  imports: [ItemComponent],
})
export class ListComponent {
  hideItemsDescription = input(false, { transform: booleanAttribute });

  items: IArticle[] = [
    {
      id: 1,
      title: 'Article 1',
      description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
    },
    {
      id: 2,
      title: 'Article 2',
      description:
        'Duis felis massa, maximus nec sapien at, maximus cursus enim.',
    },
    {
      id: 3,
      title: 'Article 3',
      description: 'Cras commodo congue neque eget accumsan.',
    },
  ];
}

@Component({
  selector: 'app-page-main',
  template: `<main>
    <app-page-list [hideItemsDescription]="hideItemsDescription" />
    <div>#pagination</div>
  </main>`,
  standalone: true,
  imports: [ListComponent],
})
export class MainComponent {
  hideItemsDescription = input(false, { transform: booleanAttribute });
}

@Component({
  selector: 'app-page',
  template: `
  <div>
    <app-page-main hideItemsDescription="true" />
    <aside>#aside</aside>
  </div>
  `,
  standalone: true,
  imports: [MainComponent],
})
export class PageComponent {}

interface IArticle {
  id: number;
  title: string;
  description: string;
}

You can observe that the hideItemsDescription input is passed from app-page through app-page-main and app-page-list, and is only used within app-item.

Input drilling can become unwieldy with complex components and numerous inputs. Two effective solutions to this problem are Content Projection with Templates and Dependency Injection.

Content Projection allows you to define a placeholder in a component's template and fill it with content from the parent component. By using templates, you can eliminate the need for Input drilling. This approach enhances reusability and adheres to the Single Responsibility Principle.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
import {
  Component,
  input,
  signal,
  computed,
  booleanAttribute,
  contentChild,
  TemplateRef,
} from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';

@Component({
  selector: 'app-item',
  template: `
  <article>
    <h1>{{ title() }}</h1>
    @if (!hideDescription()) {
      <p>{{ description() }}</p>
    }
  </article>
  `,
  standalone: true,
})
export class ItemComponent {
  data = input.required<IArticle>();
  title = computed(() => this.data().title);
  description = computed(() => this.data().description);
  hideDescription = input(false, {
    transform: booleanAttribute,
  });
}

@Component({
  selector: 'app-page-list',
  template: `
    @for (item of items; track item.id) {
      <ng-container 
        *ngTemplateOutlet="itemTemplate() || defaultTemplate; context: {$implicit: item}"
      ></ng-container>
    }

    <ng-template #defaultTemplate let-data>
      <app-item [data]="data" />
    </ng-template>
  `,
  standalone: true,
  imports: [NgTemplateOutlet, ItemComponent],
})
export class ListComponent {
  itemTemplate = contentChild<TemplateRef<IArticle>>('itemTemplate', {
    descendants: true,
  });

  items: IArticle[] = [
    {
      id: 1,
      title: 'Article 1',
      description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
    },
    {
      id: 2,
      title: 'Article 2',
      description:
        'Duis felis massa, maximus nec sapien at, maximus cursus enim.',
    },
    {
      id: 3,
      title: 'Article 3',
      description: 'Cras commodo congue neque eget accumsan.',
    },
  ];
}

@Component({
  selector: 'app-page-main',
  template: `<main>
    <ng-content></ng-content>
    <div>#pagination</div>
  </main>`,
  standalone: true,
})
export class MainComponent {}

@Component({
  selector: 'app-page',
  template: `
  <div>
    <button (click)="onButtonClick()">
      {{ hideDescription() ? 'SHOW DESCRIPTION' : 'HIDE DESCRIPTION' }}
    </button>
    <app-page-main>
      <app-page-list>
        <ng-template #itemTemplate let-data>
          <app-item [data]="data" [hideDescription]="hideDescription()"/>
        </ng-template>
      </app-page-list>
    </app-page-main>
    <aside>#aside</aside>
  </div>
  `,
  standalone: true,
  imports: [MainComponent, ListComponent, ItemComponent],
})
export class PageComponent {
  hideDescription = signal(false);

  onButtonClick() {
    this.hideDescription.update((value) => !value);
  }
}

interface IArticle {
  id: number;
  title: string;
  description: string;
}

In the example, we remove Inputs and use Content Projection from app-page to app-page-list with a Template. The template is provided with item data, allowing you to render the item as desired from the app-page.

Angular's Dependency Injection system can be complex, but it is a powerful tool for managing dependencies. Think of it as a tree, where each node is an Injector. Components with providers create their own Injectors, while those without use the nearest ancestor's Injector.

Angular Component Tree with Injection Context

Figure: Angular Component Tree with Injection Context

When a component requests a dependency, the associated Injector searches up the tree until it finds the dependency or reaches the root Injector.

Angular Component Tree with Injection Context

Figure: Angular Component Tree with Injection Context

Understanding this system is crucial, as it is widely used in Angular applications and libraries like Angular Material. I highly recommend reading the Dependency Injection documentation to gain a deeper understanding of this powerful system.

Instead of using Inputs, you can replace them with Providers to pass data.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
import {
  Component,
  input,
  inject,
  computed,
  booleanAttribute,
  InjectionToken,
} from '@angular/core';

const HIDE_DESCRIPTION_TOKEN = new InjectionToken<boolean>(
  'ItemHideDescription'
);

@Component({
  selector: 'app-item',
  template: `
  <article>
    <h1>{{ title() }}</h1>
    @if (!(_injectedHideDescription || _inputHideDescription())) {
      <p>{{ description() }}</p>
    }
  </article>
  `,
  standalone: true,
})
export class ItemComponent {
  data = input.required<IArticle>();
  title = computed(() => this.data().title);
  description = computed(() => this.data().description);
  _inputHideDescription = input(false, {
    transform: booleanAttribute,
    alias: 'hideDescription',
  });
  _injectedHideDescription = inject(HIDE_DESCRIPTION_TOKEN, { optional: true });
}

@Component({
  selector: 'app-page-list',
  template: `
  @for (item of items; track item.id) {
    <app-item [data]="item" />
  }
  `,
  standalone: true,
  imports: [ItemComponent],
})
export class ListComponent {
  items: IArticle[] = [
    {
      id: 1,
      title: 'Article 1',
      description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
    },
    {
      id: 2,
      title: 'Article 2',
      description:
        'Duis felis massa, maximus nec sapien at, maximus cursus enim.',
    },
    {
      id: 3,
      title: 'Article 3',
      description: 'Cras commodo congue neque eget accumsan.',
    },
  ];
}

@Component({
  selector: 'app-page-main',
  template: `<main>
    <app-page-list />
    <div>#pagination</div>
  </main>`,
  standalone: true,
  imports: [ListComponent],
})
export class MainComponent {}

@Component({
  selector: 'app-page',
  template: `
  <div>
    <app-page-main />
    <aside>#aside</aside>
  </div>
  `,
  standalone: true,
  imports: [MainComponent],
  providers: [
    {
      provide: HIDE_DESCRIPTION_TOKEN,
      useValue: true,
    },
  ],
})
export class PageComponent {}

interface IArticle {
  id: number;
  title: string;
  description: string;
}

You can see that in app-page we provided a static value for HIDE_DESCRIPTION_TOKEN. Therefore, each descendent of the app-page will have access to this provider, thanks to the Dependency Injection System. Now that the app-item has access to the Provider, there is no need for Input Drilling.

If dynamic changes are needed, you can use a Signal to update values.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
import {
  Component,
  input,
  inject,
  signal,
  computed,
  booleanAttribute,
  InjectionToken,
  type WritableSignal,
} from '@angular/core';

const HIDE_DESCRIPTION_TOKEN = new InjectionToken<WritableSignal<boolean>>(
  'ItemHideDescription'
);

@Component({
  selector: 'app-item',
  template: `
  <article>
    <h1>{{ title() }}</h1>
    @if (!hideDescription()) {
      <p>{{ description() }}</p>
    }
  </article>
  `,
  standalone: true,
})
export class ItemComponent {
  data = input.required<IArticle>();
  title = computed(() => this.data().title);
  description = computed(() => this.data().description);
  _inputHideDescription = input(false, {
    transform: booleanAttribute,
    alias: 'hideDescription',
  });
  _injectedHideDescription = inject(HIDE_DESCRIPTION_TOKEN, { optional: true });
  hideDescription = computed(
    () =>
      this._inputHideDescription() ||
      (this._injectedHideDescription && this._injectedHideDescription())
  );
}

@Component({
  selector: 'app-page-list',
  template: `
  @for (item of items; track item.id) {
    <app-item [data]="item" />
  }
  `,
  standalone: true,
  imports: [ItemComponent],
})
export class ListComponent {
  items: IArticle[] = [
    {
      id: 1,
      title: 'Article 1',
      description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
    },
    {
      id: 2,
      title: 'Article 2',
      description:
        'Duis felis massa, maximus nec sapien at, maximus cursus enim.',
    },
    {
      id: 3,
      title: 'Article 3',
      description: 'Cras commodo congue neque eget accumsan.',
    },
  ];
}

@Component({
  selector: 'app-page-main',
  template: `<main>
    <app-page-list />
    <div>#pagination</div>
  </main>`,
  standalone: true,
  imports: [ListComponent],
})
export class MainComponent {}

@Component({
  selector: 'app-page',
  template: `
  <div>
    <button (click)="onButtonClick()">
      {{ hideDescription() ? 'SHOW DESCRIPTION' : 'HIDE DESCRIPTION' }}
    </button>
    <app-page-main />
    <aside>#aside</aside>
  </div>
  `,
  standalone: true,
  imports: [MainComponent],
  providers: [
    {
      provide: HIDE_DESCRIPTION_TOKEN,
      useValue: signal(false),
    },
  ],
})
export class PageComponent {
  hideDescription = inject(HIDE_DESCRIPTION_TOKEN);

  onButtonClick() {
    this.hideDescription.update((value) => !value);
  }
}

interface IArticle {
  id: number;
  title: string;
  description: string;
}

By using Providers, you maintain clean and efficient code while allowing for dynamic updates to component properties from the root component.

While Inputs are a powerful feature in Angular, they are not always the best solution. By understanding and utilizing Content Projection and Dependency Injection, you can create more efficient and maintainable applications. These techniques help avoid Input drilling and adhere to best practices in software design.