| |

Angular 50 🅰️ Signal-Based Components

A signal-based component is a component whose state is expressed with signals, whose inputs are signals, whose outputs are signal-aware, and whose template reads the signals directly. The change detection is driven by the signals, and the component is checked only when the signals it reads change. The model is the modern Angular, and it replaces the older @Input/@Output decorators, the ngOnChanges lifecycle, and the BehaviorSubject-plus-subscription pattern. The result is a component that is simpler to write, easier to reason about, and more efficient at runtime. This chapter covers the signal-based component model: the input, output, and model functions that replace the decorators, the viewChild and contentChild signal queries, the linkedSignal for state that derives from an input, the resource API for the async data, and the change detection behavior that makes the model work. It is the culmination of the signal material from Angular 47 through 49.

Key point: A signal-based component uses input() for the inputs, output() for the outputs, model() for the two-way bindings, viewChild() and contentChild() for the queries, and linkedSignal() for state that resets when an input changes. The inputs are read-only signals, and the component reacts to their changes through computed and effect, not ngOnChanges. The outputs are OutputEmitterRef, and the template binds them with the event syntax. The queries return signals, and the element is available when the signal is read. The resource() API wraps an async operation and exposes value, status, and error as signals. The whole model is reactive, and the change detection is signal-driven.


Why signal-based components

The decorator-based component is the classic Angular. The @Input decorator marks a property as an input, the @Output decorator marks an EventEmitter as an output, and the ngOnChanges lifecycle reacts to the input changes.

// The classic model
@Component({ selector: 'app-user-card', template: `{{ user.name }}` })
export class UserCardComponent implements OnChanges {
  @Input() user!: User;
  @Output() selected = new EventEmitter<User>();

  ngOnChanges(changes: SimpleChanges): void {
    if (changes['user']) {
      console.log('user changed', this.user);
    }
  }

  select(): void {
    this.selected.emit(this.user);
  }
}

The model works, but it has friction: the ! on the input, the SimpleChanges type, the string key in changes['user'], the EventEmitter with its subscribe behavior. Each is a small piece of ceremony that the signal model removes.

The signal model.

// The signal model
@Component({ selector: 'app-user-card', template: `{{ user().name }}` })
export class UserCardComponent {
  readonly user = input.required<User>();
  readonly selected = output<User>();

  constructor() {
    effect(() => {
      console.log('user changed', this.user());
    });
  }

  select(): void {
    this.selected.emit(this.user());
  }
}

The input.required<User>() declares the input, and the user() is a read-only signal. The output<User>() declares the output, and the emit sends the value. The effect reacts to the input change, and no ngOnChanges is needed.

Why the signal model is simpler. The input is a signal, and the read is user(). The changes are observed through the signal’s reactions — computed, effect — not through a lifecycle hook. The types are precise, and the ceremony is gone.

Why the signal model is more efficient. The signal-based component is checked only when the signals it reads change. The decorator-based component is checked on every change detection cycle unless it uses OnPush and the inputs are immutable. The signal model produces the targeted checking by default.

Why the signal model is the modern recommendation. The Angular team introduced the signal-based APIs in v17 and recommends them for new code. The decorators remain for backward compatibility, and the two can coexist in the same project. The migration is gradual, and the new components use the signals.

Why the signal model is the future. The signal-based APIs are the direction of the framework. The zoneless change detection, the resource API, and the signal-based forms are all built on the signal model. The components that use the signals are the components that get the new features first.


The input function

The input function declares an input. It returns a read-only signal whose value is the input’s value.

@Component({ selector: 'app-user-card', template: `{{ user().name }}` })
export class UserCardComponent {
  readonly user = input.required<User>();
  readonly role = input<string>('guest');
  readonly disabled = input(false, { transform: booleanAttribute });
}

The input.required<User>() declares a required input. The input<string>('guest') declares an optional input with a default. The input(false, { transform }) declares an input with a transform.

Why the required input uses input.required. The input.required<T>() form declares an input that must be provided. The signal’s type is Signal<T>, not Signal<T | undefined>, because the input is guaranteed. The template can read user().name without a check.

Why the optional input has a default. The input<T>(default) form declares an optional input with a default value. The signal’s type is Signal<T>, and the default is used when the input is not provided.

Why the transform is a function. The transform option is a function that converts the input value to the signal’s value. The booleanAttribute transform converts the string 'true' to the boolean true, which is the pattern for the boolean attributes.

import { booleanAttribute } from '@angular/core';
readonly disabled = input(false, { transform: booleanAttribute });

Why the input is read-only. The input’s value is set by the parent, and the child cannot write it. The Signal<T> type is read-only, and the set is not available. The input is a one-way binding from the parent to the child.

Why the input can be aliased. The alias option changes the input’s name in the template.

readonly user = input.required<User>({ alias: 'userData' });

The parent binds [userData]="user", and the child reads user(). The alias is for the cases where the template name should differ from the property name.

Why the input is read in the constructor and the template. The input’s signal is available in the constructor, and the reads are tracked. The effect in the constructor can read the input, and the template can read it. The read is the same in both.

Why the input’s change is observed through the signals. The input’s change is a signal change, and the reactions are the computed and the effect. The ngOnChanges is not needed, and the change is observed through the signal’s reactions. The model is uniform.


The output function

The output function declares an output. It returns an OutputEmitterRef whose emit method sends the value.

@Component({
  selector: 'app-user-card',
  template: `<button (click)="select()">Select</button>`,
})
export class UserCardComponent {
  readonly user = input.required<User>();
  readonly selected = output<User>();

  select(): void {
    this.selected.emit(this.user());
  }
}

The output<User>() declares an output that emits a User. The emit method sends the value, and the parent binds (selected)="onSelect($event)".

Why the output is not an EventEmitter. The OutputEmitterRef is the signal-based version. It has an emit method, and it does not have the subscribe method that the EventEmitter inherits from Subject. The output is an event, not a stream, and the API reflects that.

Why the output is typed. The output<User>() declares the emitted value’s type. The emit accepts a User, and the parent’s $event is typed as User. The type flows through the binding.

Why the output can have an alias. The output<T>({ alias: 'select' }) form changes the output’s name in the template. The alias is for the cases where the template name should differ from the property name.

Why the output’s emit is synchronous. The emit calls the parent’s handler synchronously, which is the same as the EventEmitter‘s behavior. The output is not a stream, and the emit is a direct call.

Why the output’s value can be a signal. The output can emit a signal’s value, which is the pattern for the components that expose their state.

readonly valueChange = output<number>();
readonly value = signal(0);

increment(): void {
  this.value.update((v) => v + 1);
  this.valueChange.emit(this.value());
}

The output emits the new value, and the parent reacts. The pattern is the same as the classic, with the signal as the state.

Why the output is not an input. The output is a one-way binding from the child to the parent. The child emits, and the parent handles. The two-way binding is the model function, covered next.


The model function

The model function declares a two-way binding. It returns a writable signal whose value can be read and written by both the parent and the child.

@Component({
  selector: 'app-counter',
  template: `
    <button (click)="decrement()">-</button>
    <span>{{ value() }}</span>
    <button (click)="increment()">+</button>
  `,
})
export class CounterComponent {
  readonly value = model(0);

  increment(): void {
    this.value.update((v) => v + 1);
  }

  decrement(): void {
    this.value.update((v) => v - 1);
  }
}

The parent binds [(value)]="count", and the child reads and writes value(). The two-way binding is the model.

Why the model is a writable signal. The child can write to the model, and the write propagates to the parent. The WritableSignal<T> type is the model’s type, and the set and update are available.

Why the model emits a change. The model has an implicit output that fires when the value is written. The parent’s [(value)] binding is the shorthand for [value]="count" (valueChange)="count = $event". The model’s output is the valueChange, and the parent’s binding is the two-way.

Why the model has a required form. The model.required<T>() form declares a required two-way binding. The signal’s type is WritableSignal<T>, and the parent must provide the binding.

Why the model is the two-way binding. The [(value)] syntax is the Angular two-way binding, and the model function is the signal-based implementation. The classic @Input plus @Output with the valueChange name is the decorator-based version. The signal version is simpler and more type-safe.

Why the model can be aliased. The model<T>({ alias: 'value' }) form changes the name. The alias is for the cases where the template name should differ from the property name.

Why the model is the modern replacement for the @Input + @Output pair. The classic two-way binding required an input, an output with the Change suffix, and a method that emits the change. The model function is the single declaration, and the two-way binding is the shorthand. The model is the modern pattern.

Why the model is used sparingly. A two-way binding is a coupling between the parent and the child. The child can change the parent’s state, which is a strong relationship. The input and the output are the looser alternatives, and the model is for the cases where the two-way binding is the intent.


Signal queries

The viewChild, viewChildren, contentChild, and contentChildren functions are the signal-based queries. They return signals whose values are the queried elements or components.

@Component({
  selector: 'app-form',
  template: `<input #nameInput /><button (click)="focus()">Focus</button>`,
})
export class FormComponent {
  readonly nameInput = viewChild<ElementRef<HTMLInputElement>>('nameInput');

  focus(): void {
    this.nameInput()?.nativeElement.focus();
  }
}

The viewChild<ElementRef<HTMLInputElement>>('nameInput') queries the template reference variable #nameInput, and the signal’s value is the ElementRef. The focus method reads the signal and accesses the element.

Why the query returns a signal. The queried element is available after the view is initialized, which is after the constructor. The signal’s value is set when the element is available, and the reads in the template or the effect see the value. The signal is the modern replacement for the @ViewChild decorator and the AfterViewInit lifecycle.

Why the signal’s type includes undefined. The element is not available until the view is initialized, and the query may not match. The signal’s type is Signal<ElementRef<HTMLInputElement> | undefined>, and the read must handle the undefined. The viewChild.required form declares a query that must match, and the signal’s type is non-undefined.

Why the query is read in the effect. The query’s value is set after the view is initialized, and the effect runs after the initialization. The effect’s read sees the value, and the side effect — the focus, the DOM manipulation — runs when the element is available.

constructor() {
  effect(() => {
    this.nameInput()?.nativeElement.focus();
  });
}

Why the viewChildren returns a signal of an array. The plural form queries all the matching elements, and the signal’s value is the array. The array is read-only, and the elements are in the order they appear in the template.

Why the contentChild and contentChildren query the projected content. The viewChild queries the component’s own template, and the contentChild queries the content projected into the component. The two are separate, and the choice depends on what is being queried.

Why the signal query is the modern replacement for the decorator. The classic @ViewChild('nameInput') nameInput!: ElementRef required the !, the AfterViewInit lifecycle, and the ngAfterViewInit method. The signal query returns a signal, and the read handles the availability. The model is simpler.

Why the signal query is the reactive query. The signal’s value is set when the element is available, and the reaction is the effect or the template. The query is part of the signal graph, and the change detection is driven by the signals. The model is uniform.


The linkedSignal function

The linkedSignal function declares a writable signal whose value is derived from another signal but can be written independently. It is the state that resets when the source changes.

@Component({ selector: 'app-select', template: `...` })
export class SelectComponent {
  readonly options = input.required<string[]>();
  readonly selected = linkedSignal(() => this.options()[0] ?? '');
}

The selected is derived from the options input, and the initial value is the first option. When the options input changes, the selected resets to the new first option. But the selected can also be written by the user’s selection, which overrides the derivation until the options changes again.

Why the linkedSignal is the pattern for the reset-on-change state. The classic pattern was a BehaviorSubject for the options and a Subject for the selection, combined with switchMap and startWith. The linkedSignal is the single declaration, and the reset is automatic.

Why the linkedSignal is writable. The signal can be written by the component, and the write overrides the derivation. The next change to the source resets the value to the derivation. The writable nature is the difference from the computed.

Why the linkedSignal is the alternative to the effect that writes. The pattern of an effect that writes to a signal when an input changes is the classic mistake — the effect is the wrong tool. The linkedSignal is the correct tool: the derived value resets when the source changes, and the write is allowed.

Why the linkedSignal can have a custom computation. The linkedSignal accepts a computation that receives the previous value and the source, and returns the new value. The computation can decide whether to reset or to keep the previous value.

readonly selected = linkedSignal({
  source: this.options,
  computation: (options, previous) =>
    options.includes(previous?.value) ? previous.value : options[0],
});

The computation receives the new options and the previous state, and it returns the new value. The pattern is for the cases where the reset should be conditional.

Why the linkedSignal is the modern state pattern. The linkedSignal is the declarative way to express the “reset when the source changes, but allow the user to override” pattern. The pattern is common — a select, a filter, a tab — and the linkedSignal is the tool.

Why the linkedSignal is not a computed. The computed is read-only, and the linkedSignal is writable. The computed recomputes on every source change, and the linkedSignal resets only when the source changes. The two are for the different cases.

Why the linkedSignal is not a plain signal. The plain signal has no relationship to the source, and the reset is manual. The linkedSignal has the relationship, and the reset is automatic. The relationship is the point.


The resource API

The resource API wraps an async operation and exposes the value, the status, and the error as signals. It is the signal-based replacement for the switchMap-plus-toSignal pattern.

@Component({
  selector: 'app-users',
  template: `
    @if (users.status() === 'loading') { <p>Loading...</p> }
    @if (users.error()) { <p>Error: {{ users.error() }}</p> }
    @if (users.value(); as list) {
      @for (user of list; track user.id) { <div>{{ user.name }}</div> }
    }
  `,
})
export class UsersComponent {
  private readonly http = inject(HttpClient);
  readonly filter = signal('');

  readonly users = resource({
    request: () => ({ filter: this.filter() }),
    loader: ({ request, abortSignal }) =>
      fetch(`/api/users?q=${request.filter}`, { signal: abortSignal })
        .then((r) => r.json() as Promise<User[]>),
  });
}

The resource has a request function that produces the parameters, and a loader that performs the async operation. The signals — value, status, error — are the result.

Why the resource is the modern data fetching. The switchMap-plus-toSignal pattern was the previous approach: a signal to an Observable, a switchMap to the request, and a toSignal back. The resource is the single declaration, and the request, the loading state, and the error are all managed.

Why the resource has a request function. The request function produces the parameters for the loader, and it is tracked. When the request’s dependencies change, the loader re-runs with the new parameters, and the previous request is aborted.

Why the resource has an abortSignal. The abortSignal is passed to the loader and aborts the previous request when a new one starts. The pattern is the cancellation, and the resource provides the signal.

Why the resource exposes status. The status signal is 'idle', 'loading', 'error', 'success', or 'reloading'. The template uses the status to render the loading and error states. The status is the state machine, and the resource manages it.

Why the resource exposes error. The error signal is the error from the loader, and the template renders it. The error is the state, and the resource tracks it.

Why the resource exposes value. The value signal is the loader’s result, and the template reads it. The value is undefined until the loader completes, and the template handles the undefined.

Why the resource has a reload method. The reload method re-runs the loader with the current request. The method is for the manual refresh, and the pattern is the refresh button.

Why the resource is the modern pattern for the async data. The resource combines the request, the loading state, the error, the value, the cancellation, and the reload. The pattern is the same for every async data, and the resource is the single declaration. The switchMap-plus-toSignal is the older version, and the resource is the newer one.


Complete Example Session

import {
  Component, input, output, model, viewChild, contentChild,
  linkedSignal, resource, signal, computed, effect, inject,
  ElementRef, booleanAttribute,
} from '@angular/core';
import { HttpClient } from '@angular/common/http';

// ============================================
// PART 1: THE INPUTS
// ============================================

@Component({ selector: 'app-user-card', standalone: true, template: `` })
export class UserCardComponent {
  readonly user = input.required<User>();
  readonly role = input<string>('guest');
  readonly disabled = input(false, { transform: booleanAttribute });
  readonly userAlias = input<User | null>(null, { alias: 'userData' });

  constructor() {
    effect(() => {
      console.log('user changed', this.user());
      console.log('role', this.role());
    });
  }
}

// ============================================
// PART 2: THE OUTPUTS
// ============================================

@Component({ selector: 'app-button', standalone: true, template: `<button (click)="click()">Go</button>` })
export class ButtonComponent {
  readonly clicked = output<void>();
  readonly valueChanged = output<number>();

  click(): void {
    this.clicked.emit();
    this.valueChanged.emit(Date.now());
  }
}

// ============================================
// PART 3: THE MODEL
// ============================================

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <button (click)="decrement()">-</button>
    <span>{{ value() }}</span>
    <button (click)="increment()">+</button>
  `,
})
export class CounterComponent {
  readonly value = model(0);

  increment(): void { this.value.update((v) => v + 1); }
  decrement(): void { this.value.update((v) => v - 1); }
}

// The parent binds [(value)]="count".

// ============================================
// PART 4: THE VIEW CHILD
// ============================================

@Component({
  selector: 'app-form',
  standalone: true,
  template: `<input #nameInput /><button (click)="focus()">Focus</button>`,
})
export class FormComponent {
  readonly nameInput = viewChild<ElementRef<HTMLInputElement>>('nameInput');

  constructor() {
    effect(() => {
      this.nameInput()?.nativeElement.focus();
    });
  }

  focus(): void {
    this.nameInput()?.nativeElement.focus();
  }
}

// ============================================
// PART 5: THE LINKED SIGNAL
// ============================================

@Component({ selector: 'app-select', standalone: true, template: `` })
export class SelectComponent {
  readonly options = input.required<string[]>();
  readonly selected = linkedSignal(() => this.options()[0] ?? '');

  select(option: string): void {
    this.selected.set(option);
  }
}

// ============================================
// PART 6: THE RESOURCE
// ============================================

@Component({
  selector: 'app-users',
  standalone: true,
  template: `
    @if (users.status() === 'loading') { <p>Loading...</p> }
    @if (users.error()) { <p>Error: {{ users.error() }}</p> }
    @if (users.value(); as list) {
      @for (user of list; track user.id) { <div>{{ user.name }}</div> }
    }
    <button (click)="users.reload()">Refresh</button>
  `,
})
export class UsersComponent {
  readonly filter = signal('');

  readonly users = resource({
    request: () => ({ filter: this.filter() }),
    loader: ({ request, abortSignal }) =>
      fetch(`/api/users?q=${request.filter}`, { signal: abortSignal })
        .then((r) => r.json() as Promise<User[]>),
  });
}

// ============================================
// PART 7: THE COMPUTED ON THE RESOURCE
// ============================================

@Component({ selector: 'app-count', standalone: true, template: `` })
export class CountComponent {
  readonly users = resource({ /* ... */ });

  readonly count = computed(() => this.users.value()?.length ?? 0);
}

// ============================================
// PART 8: THE COMPONENT WITH EVERYTHING
// ============================================

@Component({
  selector: 'app-search',
  standalone: true,
  template: `
    <input [value]="term()" (input)="term.set($any($event.target).value)" />
    <button (click)="refresh()">Refresh</button>
    @if (results.status() === 'loading') { <p>Loading...</p> }
    @if (results.value(); as list) {
      @for (item of list; track item.id) {
        <div (click)="select.emit(item)">{{ item.name }}</div>
      }
    }
  `,
})
export class SearchComponent {
  readonly term = signal('');
  readonly select = output<Item>();

  readonly results = resource({
    request: () => ({ q: this.term() }),
    loader: ({ request, abortSignal }) =>
      fetch(`/api/search?q=${request.q}`, { signal: abortSignal })
        .then((r) => r.json() as Promise<Item[]>),
  });

  refresh(): void {
    this.results.reload();
  }
}

// ============================================
// PART 9: THE CONTENT CHILD
// ============================================

@Component({
  selector: 'app-panel',
  standalone: true,
  template: `<div class="panel"><ng-content /></div>`,
})
export class PanelComponent {
  readonly header = contentChild<ElementRef>('header');
}

// ============================================
// PART 10: WHAT NOT TO DO
// ============================================

// Don't use @Input and @Output in new code
// Use input() and output().

// Don't use ngOnChanges
// Use effect() or computed() on the input signal.

// Don't use @ViewChild and ngAfterViewInit
// Use viewChild().

// Don't use an effect to write a derived value
// Use linkedSignal() or computed().

// Don't use switchMap + toSignal for the async data
// Use resource().

// Don't use a plain signal for the reset-on-change state
// Use linkedSignal().

The ten parts cover the inputs, the outputs, the model, the view child, the linked signal, the resource, the computed on the resource, the full component, the content child, and the anti-patterns.


Quick Reference

The Signal-Based APIs

FunctionPurpose
input()Input signal
input.required()Required input
output()Output emitter
model()Two-way binding
viewChild()View query
viewChildren()View query (multiple)
contentChild()Content query
contentChildren()Content query (multiple)
linkedSignal()Reset-on-change state
resource()Async data

The Input Options

OptionPurpose
input<T>(default)Optional with default
input.required<T>()Required
{ alias: 'name' }Template name
{ transform: fn }Convert the value

The Output API

MethodPurpose
emit(value)Send the value
{ alias: 'name' }Template name

The Model API

MethodPurpose
value()Read
value.set(v)Write
value.update(fn)Compute
model.required<T>()Required two-way

The Resource API

Signal/MethodPurpose
value()The result
status()idle, loading, error, success, reloading
error()The error
reload()Re-run the loader

The Linked Signal

FormPurpose
linkedSignal(() => source())Reset when the source changes
linkedSignal({ source, computation })Custom reset logic

Best Practices

✅ Do This:

// Use input() for the inputs
readonly user = input.required<User>();                        // ✅

// Use output() for the outputs
readonly selected = output<User>();                            // ✅

// Use model() for the two-way bindings
readonly value = model(0);                                     // ✅

// Use viewChild() for the queries
readonly input = viewChild<ElementRef>('input');               // ✅

// Use linkedSignal() for the reset-on-change state
readonly selected = linkedSignal(() => this.options()[0]);     // ✅

// Use resource() for the async data
readonly users = resource({ request, loader });                // ✅

// Use effect() for the input change reaction
effect(() => console.log(this.user()));                        // ✅

// Use computed() for the derived value
readonly displayName = computed(() => this.user().name);       // ✅

❌ Don’t Do This:

// Don't use @Input and @Output in new code
@Input() user!: User;  // use input()                          // ⚠️

// Don't use ngOnChanges
ngOnChanges(changes: SimpleChanges) {}  // use effect()        // ⚠️

// Don't use @ViewChild and ngAfterViewInit
@ViewChild('input') input!: ElementRef;  // use viewChild()    // ⚠️

// Don't use an effect to write a derived value
effect(() => this.doubled.set(this.count() * 2));  // use computed // ⚠️

// Don't use switchMap + toSignal for the async data
toSignal(toObservable(this.filter).pipe(switchMap(fetch)));  // use resource // ⚠️

// Don't use a plain signal for the reset-on-change state
readonly selected = signal('');  // use linkedSignal           // ⚠️

// Don't forget the required input's signal type
// input.required<T>() → Signal<T>, not Signal<T | undefined> // ⚠️

Common Pitfalls

PitfallProblemSolution
@Input in new codeLegacyUse input()
ngOnChangesLegacyUse effect()
@ViewChildLegacyUse viewChild()
Effect writes a derived valueWrong toolUse linkedSignal() or computed()
switchMap + toSignalLegacyUse resource()
Plain signal for reset-on-changeManual resetUse linkedSignal()
Input type not narrowedundefinedUse input.required()
Query read in the constructorNot availableRead in the effect

Real-World Examples

1. Required input

readonly user = input.required<User>();

2. Optional input with default

readonly role = input<string>('guest');

3. Input with transform

readonly disabled = input(false, { transform: booleanAttribute });

4. Output

readonly selected = output<User>();

5. Two-way binding

readonly value = model(0);

6. View child

readonly input = viewChild<ElementRef<HTMLInputElement>>('input');

7. Linked signal

readonly selected = linkedSignal(() => this.options()[0]);

8. Resource

readonly users = resource({ request, loader });

9. Computed on the resource

readonly count = computed(() => this.users.value()?.length ?? 0);

10. Full component

@Component({
  selector: 'app-search',
  template: `
    <input [value]="term()" (input)="term.set($any($event.target).value)" />
    @if (results.value(); as list) {
      @for (item of list; track item.id) { <div>{{ item.name }}</div> }
    }
  `,
})
export class SearchComponent {
  readonly term = signal('');
  readonly results = resource({ request, loader });
}

Visual: The Signal-Based Component

┌──────────────────────────────────────────────────────────┐
│  INPUTS                                                  │
│    readonly user = input.required<User>()                │
│    readonly role = input<string>('guest')                │
│                                                          │
├──────────────────────────────────────────────────────────┤
│  STATE                                                   │
│    readonly count = signal(0)                            │
│    readonly doubled = computed(() => count() * 2)        │
│                                                          │
├──────────────────────────────────────────────────────────┤
│  TWO-WAY                                                 │
│    readonly value = model(0)                             │
│                                                          │
├──────────────────────────────────────────────────────────┤
│  QUERIES                                                 │
│    readonly input = viewChild<ElementRef>('input')       │
│                                                          │
├──────────────────────────────────────────────────────────┤
│  ASYNC                                                   │
│    readonly users = resource({ request, loader })        │
│                                                          │
├──────────────────────────────────────────────────────────┤
│  OUTPUTS                                                 │
│    readonly selected = output<User>()                    │
│                                                          │
├──────────────────────────────────────────────────────────┤
│  TEMPLATE                                                │
│    {{ user().name }} {{ doubled() }}                     │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The Input

┌──────────────────────────────────────────────────────────┐
│  CLASSIC                                                 │
│                                                          │
│  @Input() user!: User;                                   │
│    │                                                     │
│    └── the ! is a non-null assertion                     │
│        the type is User, not User | undefined            │
│        the ngOnChanges handles the change                │
│                                                          │
├──────────────────────────────────────────────────────────┤
│  SIGNAL                                                  │
│                                                          │
│  readonly user = input.required<User>();                 │
│    │                                                     │
│    └── the signal is Signal<User>                        │
│        no ! needed                                       │
│        the effect handles the change                     │
│        the computed derives from the input               │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The Two-Way Binding

┌──────────────────────────────────────────────────────────┐
│  CLASSIC                                                 │
│                                                          │
│  @Input() value!: number;                                │
│  @Output() valueChange = new EventEmitter<number>();     │
│                                                          │
│  setValue(v: number) {                                   │
│    this.value = v;                                       │
│    this.valueChange.emit(v);                             │
│  }                                                       │
│                                                          │
│  Parent: [(value)]="count"                               │
│                                                          │
├──────────────────────────────────────────────────────────┤
│  SIGNAL                                                  │
│                                                          │
│  readonly value = model(0);                              │
│                                                          │
│  setValue(v: number) {                                   │
│    this.value.set(v);  // emits the change automatically │
│  }                                                       │
│                                                          │
│  Parent: [(value)]="count"                               │
│                                                          │
│  One declaration. The change is automatic.               │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The Linked Signal

┌──────────────────────────────────────────────────────────┐
│  readonly options = input.required<string[]>();          │
│  readonly selected = linkedSignal(() => this.options()[0] ?? '');│
│                                                          │
│  INITIAL                                                 │
│    options = ['a', 'b', 'c']                             │
│    selected = 'a'  (derived from options)                │
│                                                          │
│  USER SELECTS 'b'                                        │
│    selected.set('b')                                     │
│    selected = 'b'  (overrides the derivation)            │
│                                                          │
│  OPTIONS CHANGE                                          │
│    options = ['x', 'y', 'z']                             │
│    selected = 'x'  (resets to the derivation)            │
│                                                          │
│  The selected resets when the source changes,            │
│  but the user can override it in between.                │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The Resource

┌──────────────────────────────────────────────────────────┐
│  readonly filter = signal('')                            │
│                                                          │
│  readonly users = resource({                             │
│    request: () => ({ filter: this.filter() }),           │
│    loader: ({ request, abortSignal }) => fetch(...),     │
│  });                                                     │
│                                                          │
│  users.status()  → 'idle' | 'loading' | 'error' |        │
│                     'success' | 'reloading'              │
│  users.value()   → User[] | undefined                    │
│  users.error()   → unknown                               │
│  users.reload()  → re-run the loader                     │
│                                                          │
│  filter.set('alice')                                     │
│       │                                                  │
│       ▼                                                  │
│  The request re-runs.                                    │
│  The previous request is aborted.                        │
│  status = 'loading'                                      │
│       │                                                  │
│       ▼                                                  │
│  The loader completes.                                   │
│  value = the result                                      │
│  status = 'success'                                      │
│                                                          │
└──────────────────────────────────────────────────────────┘

Visual: The Query

┌──────────────────────────────────────────────────────────┐
│  CLASSIC                                                 │
│                                                          │
│  @ViewChild('input') input!: ElementRef;                 │
│                                                          │
│  ngAfterViewInit() {                                     │
│    this.input.nativeElement.focus();                     │
│  }                                                       │
│                                                          │
│  The element is available after the view init.           │
│                                                          │
├──────────────────────────────────────────────────────────┤
│  SIGNAL                                                  │
│                                                          │
│  readonly input = viewChild<ElementRef>('input');        │
│                                                          │
│  constructor() {                                         │
│    effect(() => {                                        │
│      this.input()?.nativeElement.focus();                │
│    });                                                   │
│  }                                                       │
│                                                          │
│  The signal's value is set when the element is available.│
│  The effect reacts to the availability.                  │
│                                                          │
└──────────────────────────────────────────────────────────┘

Summary

APIPurpose
input()Input signal
output()Output emitter
model()Two-way binding
viewChild()View query
contentChild()Content query
linkedSignal()Reset-on-change state
resource()Async data
effect()Side effect
computed()Derived value
signal()State
ClassicSignal
@Input()input()
@Output()output()
@Input + @Output Changemodel()
@ViewChild()viewChild()
@ContentChild()contentChild()
ngOnChangeseffect()
ngAfterViewIniteffect() on the query
switchMap + toSignalresource()

Key takeaways:

  • The signal-based component uses input(), output(), model(), and the signal queries — the decorators are the legacy, and the signal APIs are the modern
  • The inputs are read-only signals — they are read with () and reacted to with computed and effect, and no ngOnChanges is needed
  • The input.required narrows the type — the signal is Signal<T>, not Signal<T | undefined>, and the template does not need a check
  • The outputs are OutputEmitterRef — the emit sends the value, and the API is not the EventEmitter‘s stream
  • The model is the two-way binding — the [(value)] syntax is the shorthand, and the change is automatic
  • The signal queries replace the @ViewChild and the AfterViewInit — the query returns a signal whose value is set when the element is available
  • The linkedSignal is the reset-on-change state — the value derives from a source and resets when the source changes, but the component can write it in between
  • The resource API is the async data — the request produces the parameters, the loader performs the operation, and the value, status, and error are signals
  • The effect reacts to the input changes — it is the replacement for the ngOnChanges, and it is the tool for the side effects
  • The computed derives the values — it is the replacement for the method-in-the-template, and the caching is the improvement

Remember: The signal-based component is the modern Angular. The inputs are input(), the outputs are output(), the two-way bindings are model(), the queries are viewChild() and contentChild(), the state is signal(), the derived values are computed(), the side effects are effect(), the reset-on-change state is linkedSignal(), and the async data is resource(). The whole model is reactive, and the change detection is signal-driven. The decorators are the legacy, and the signals are the future. The component that uses the signals is the component that is simpler, more efficient, and ready for the zoneless Angular.


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!