angularangular-signals

How to derive state from required inputs without ngOnInit and constructor?


I'm refactoring an Angular component to use signals throughout, but I'm stuck on how to properly initialize a signal that depends on required inputs without using ngOnInit. I am trying to see in this experiment that how it behaves without using any lifecycle hooks.

The processedData signal needs to be mutable, but its initial state must be derived from the required inputs which aren't available in the constructor.

What's the recommended Angular signals pattern for deriving initial signal state from required inputs while keeping the signal mutable for user interactions? I don't want to see this error if not using ngOnInit lifecycle:

RuntimeError: NG0950: Input "items" is required but no value is available yet. Find more at https://angular.dev/errors/NG0950

Current approach (works but uses ngOnInit):

import { Component, input, output, signal } from '@angular/core';
import { explicitEffect } from 'ngxtension/explicit-effect';

interface DataItem {
  id: string;
  name: string;
  status: string;
}

type ProcessedData = Record<string, {
  item: DataItem;
  selected: boolean;
  displayName: string;
}>;

@Component({
  selector: 'app-example',
  template: `<div>{{ processedData() | json }}</div>`
})
export class ExampleComponent {
  items = input.required<DataItem[]>();
  mode = input.required<'add' | 'remove'>();
  
  processedData = signal<ProcessedData>({});
  dataChanged = output<ProcessedData>();

  constructor() {
    explicitEffect([this.processedData], ([data]) => 
      this.dataChanged.emit(data)
    );
  }
  
  ngOnInit(): void { //trying to avoid lifecycles 
    this.processedData.set(
      this.items().reduce((acc, item) => ({
        ...acc,
        [item.id]: {
          item,
          selected: this.getInitialState(item),
          displayName: item.name.toUpperCase()
        }
      }), {})
    );
  }
  
  updateSelection(id: string): void {
    this.processedData.update(data => ({
      ...data,
      [id]: { ...data[id], selected: !data[id].selected }
    }));
  }
  
  private getInitialState(item: DataItem): boolean {
    return this.mode() === 'add' 
      ? item.status === 'ACTIVE'
      : item.status === 'FINAL';
  }
}

Solution

  • Use linkedSignal, it derives its value based on a signal and also you can later change it or mutate it.

    @Component({...})
    export class Component {
      items = input.required<DataItem[]>();
      processedData = linkedSignal(() => this.items().reducer(...));
    
      // mutate it whenever you need
      someFunction() {
        this.processedData.set(...);
      }
    }
    

    With this approach, you can bypass the usage of ngOnInit.

    Check the following demo

    You can read more about it here