Skip to content

Prefer Logic-Free Components

Complex logic inside of the component class is brittle, difficult to test, and leads to components that are hard to deconstruct into smaller more manageable (and sometimes resusable) components.

The ideal is a component that has only a single line of code that imports a Signal-based service (we prefer the NGRX SignalStore). The template directly references this store. Templates are the "imperative shell" of our Angular applications - in other words, decisions (@if(), @switch()) and loops (@for()) are fine here.

An Example

typescript
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { CounterStore } from '../services/counter.store';
import { ButtonDirective } from '@shared';

@Component({
  selector: 'app-counter-ui',
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [ButtonDirective],
  template: `
    <div class="mt-12 ">
      <button
        class="mr-4 btn-square"
        [disabled]="store.decrementDisabled()"
        (click)="store.decrement()"
        appButton
        shape="circle"
        kind="secondary"
      >
        -
      </button>
      <span data-testid="current">{{ store.current() }}</span>
      <button
        class="ml-4"
        (click)="store.increment()"
        appButton
        shape="circle"
        [kind]="store.current() > 20 ? 'primary' : 'error'"
      >
        +
      </button>
    </div>
    <div>
      @switch (store.fizzBuzz()) {
        @case ('Fizz') {
          <p class="font-bold text-2xl text-green-400">Fizz</p>
        }
        @case ('Buzz') {
          <p class="font-bold text-2xl text-orange-400">Buzz</p>
        }
        @case ('FizzBuzz') {
          <p class="font-bold text-3xl text-green-800 animate-pulse">
            FIZZBUZZ!
          </p>
        }
      }
    </div>
  `,
  styles: ``,
})
export class UiComponent {
  store = inject(CounterStore);
}