Skip to content

Angular Component State

Most state in an Angular application lives locally in a single component: a form's draft values, whether a dropdown is open, the currently selected tab. This lesson covers modeling that state well.

What Is Component State?

Component state is data that only one component (and possibly its direct children, via inputs) needs to know about. It doesn't need to be shared through a service or synchronized across unrelated parts of the app.

Modern Angular models this kind of state naturally with signals: plain class fields for simple cases, signal() for state templates need to react to reactively, and computed() for values derived from other state.

@Component({
  selector: 'app-accordion',
  standalone: true,
  template: `
    <button (click)="toggle()">{{ isOpen() ? 'Hide' : 'Show' }} Details</button>
    @if (isOpen()) {
      <div class="details"><ng-content></ng-content></div>
    }
  `,
})
export class AccordionComponent {
  isOpen = signal(false);
  toggle() {
    this.isOpen.update(open => !open);
  }
}

isOpen is purely local, no other component in the app needs to know or care about this accordion's open state.

Modeling Local State

count = 0;                          // plain field, fine for non-reactive/static values
count = signal(0);                   // reactive field, template updates automatically
doubled = computed(() => this.count() * 2);  // derived from other signals
  • Plain class fields work for values the template reads once (like configuration) or that don't need automatic reactivity.
  • signal() is the standard way to model reactive local state that the template should re-render on change.
  • computed() derives a new reactive value from one or more signals, recalculating only when its dependencies change.
  • Avoid duplicating derived state as its own signal, if it can be computed, use computed() instead.

Component State Cheatsheet

Common local state patterns in Angular components.

Pattern Example Use Case
Plain field title = 'Dashboard'; Static or rarely-changing values
Signal count = signal(0); Reactive state the template renders
Computed total = computed(() => a() + b()); Derived values from other signals
Effect effect(() => console.log(count())); Reacting to signal changes with a side effect
Two-way with a form control [(ngModel)] / reactive forms Editable local form state

Deriving State With computed()

Rather than manually keeping a second signal in sync with a first (updating both every time something changes), computed() derives its value automatically and only recalculates when one of its dependencies actually changes.

export class CartSummaryComponent {
  items = signal<{ price: number; qty: number }[]>([]);

  total = computed(() =>
    this.items().reduce((sum, item) => sum + item.price * item.qty, 0)
  );
}

total always reflects items correctly, there's no risk of it becoming stale after an update.

Reacting to State Changes With effect()

effect() runs a function automatically whenever any signal it reads changes, useful for side effects like logging, syncing to localStorage, or triggering an animation, rather than for computing new state (use computed() for that instead).

export class ThemeToggleComponent {
  isDark = signal(false);

  constructor() {
    effect(() => {
      document.body.classList.toggle('dark-theme', this.isDark());
    });
  }
}

Lifting State Up When Siblings Need It

When two sibling components both need access to the same piece of state, the common pattern (borrowed from component-based UI generally) is to "lift" that state up to their shared parent, and pass it down via inputs/outputs, rather than reaching for a full shared service prematurely.

// parent owns the shared state
@Component({ selector: 'app-page', standalone: true, imports: [FilterBarComponent, ResultsListComponent], template: `
  <app-filter-bar [filter]="filter()" (filterChange)="filter.set($event)" />
  <app-results-list [filter]="filter()" />
` })
export class PageComponent {
  filter = signal('');
}

Common Mistakes

  • Reaching for a shared service the moment two components need related data, when lifting state to a common parent would be simpler.
  • Manually recomputing and storing a derived value in a second signal instead of using computed().
  • Using effect() to compute new state (causing subtle update loops) instead of computed().
  • Keeping too much state as plain, non-reactive fields, then wondering why the template doesn't update.

Key Takeaways

  • Component state is data scoped to a single component (and possibly its direct children).
  • Signals (signal()) model reactive local state; computed() derives values from other signals automatically.
  • effect() is for side effects reacting to state changes, not for computing new state.
  • Lift state up to a shared parent before reaching for a full shared service, when siblings need the same data.

Pro Tip

If you find yourself manually keeping two signals in sync with an effect, stop and check whether one of them should actually be a computed() derived from the other, that's almost always the simpler, more correct approach.