Imagine you're maintaining an Angular application that started years ago, before Signals existed. You've kept up with the upgrade path, you're now on Angular 20 or 21, and naturally the question comes up:

Should you migrate parts of your codebase to Signals?

The answer is: not necessarily. A full rewrite rarely makes sense. But in many cases, selectively introducing Signals can simplify your architecture, reduce boilerplate, and make data flow easier to reason about.

The real question becomes:

Which parts of your existing RxJS-driven app are actually worth migrating?

In this serie of articles, we'll focus on one thing only: identifying the areas where switching to Signals delivers the highest impact, and how to migrate them safely and incrementally.

And today, we will focus on the handling of component inputs, outputs, and how to manage component local state.

1. Why is this the first step?

If your app was built before Angular introduced Signals, a big portion of your component communication still relies on the classic @Input() / @Output(), sometimes mixed with ngOnChanges and imperative patches.

Migrate all Inputs and Outputs to input() and output() Signals using the official Angular schematics will give your codebase an immediate "signal surface area", without rewriting anything else yet:

  • They're 100% backward-compatible They don't change how your component is used from the outside.

  • They give you instant signal-based reactivity A @Input() that used to require ngOnChanges, manual coercion, or tracking logic becomes a reactive signal you can read directly.

  • Angular provides automatic migration schematics You don't need to hand-edit hundreds of components. The CLI does the work for you.

  • They unlock the next step: computed values As soon as your Inputs are signals, your derived logic becomes way easier to write and understand.

2. Replace getters and setters

A common pattern in older Angular apps was:

  • receive an input
  • transform it inside a setter
  • store the transformed value locally

Example:

private _rawDate!: string;
formattedDate!: string;

@Input() set date(value: string) {
  this._rawDate = value;
  this.formattedDate = this.format(value);
}

private format(date: string): string {
  return new Date(date).toLocaleDateString();
}

Typical drawbacks:

  • imperative flow
  • local mutable state
  • the real source of truth is split between _rawDate and formattedDate
  • easy to lose track of what updates what

Now let's take the same feature and migrate it with signal API:

date = input<string>();

formattedDate = computed(() =>
  new Date(this.date()).toLocaleDateString()
);

What you gain:

  • no lifecycle hook or setter logic
  • the transformed value is expressed declaratively
  • state stays consistent
  • much easier to read, test, and maintain

3. ngOnChanges lifecycle hook replacement

A very typical pattern in "legacy" components is, when the input changes:

  • Recompute a formatted value
  • Trigger a side effect (logging, API call, analytics, service sync…)
  • All of the above
@Input() date!: string;

formattedDate!: string;

ngOnChanges(changes: SimpleChanges) {
  if (changes.date) {
    // Recompute value
    this.formattedDate = new Date(this.date).toLocaleDateString();

    // Trigger side effect (here I just perform a simple console.log for keeping the example small and readable)
    console.log('Date changed:', this.formattedDate);
  }
}

Typical drawbacks (we have already covered some of them in the previous example):

  • Imperative, lifecycle-driven logic
  • Derived state (formattedDate) is mutable and can drift
  • Side effects are hidden inside Angular's lifecycle
  • Harder to test, mock, or reuse

Signals make it natural to separate concerns:

  • computed() holds all pure, derived logic
  • effect() reacts to changes and triggers side effects

Let's re-write this feature using signals API:

date = input<string>();

formattedDate = computed(() =>
  new Date(this.date()).toLocaleDateString()
);

logDateEffect = effect(() => {
  console.log('Date changed:', this.formattedDate());
});

What you gain:

  • Declarative & readonly formattedDate
  • The side effect logic is explicit and colocated with the state it depends on
  • No lifecycle hooks, no SimpleChanges
  • Much easier mental model: state => derived state => effects

Conclusion

In conclusion, this migration path is easy to apply and comes with very little risk. Updating Inputs and Outputs is mostly handled by the schematics, and it quickly removes a lot of old patterns like ngOnChanges or extra getters that no longer make sense with Signals.

It also brings clear improvements in change detection, making your app more efficient and predictable. And the best part is that you can do all of this without touching your services or your RxJS logic yet. It's a small, incremental step that already makes the code cleaner and easier to maintain.

In the next article, we will look at how to replace the common pattern of sharing state in a service via a Behavior Subject with Signals, for a simpler and more reactive approach.