DEV Community

Cover image for Signals vs. ngOnChanges for better Angular state management
Megan Lee for LogRocket

Posted on • Originally published at blog.logrocket.com

Signals vs. ngOnChanges for better Angular state management

Written by Lewis Cianci✏️

You know what framework just hasn’t stayed still lately? Angular. For a long time, nothing really seemed to change, and now we’re smack-bang in the middle of what some are calling the Angular Renaissance.

First up, we received signals, then control flow programming was added to templates. And now, signals are continuing to grow in Angular, spreading their wings into the area of state management.

Signals in components are now available in developer preview, and no doubt will be available for use in stable Angular before long.

Why the change in Angular?

One of the core design principles of Angular, as compared to something like React, relates to how changes to the view are handled.

In a library like React, developers could modify properties that should be displayed on the page. However, the view would not update until setState() was called. Making the developer responsible for telling the framework when to redraw components can lead to a somewhat harder DX, but can also yield performance benefits.

Angular takes a different route by using data binding in the view. When a variable is updated in code, this causes Angular to redraw the affected view to reflect the changes. The developer doesn’t have to manually call something like setState(), as the framework tries to work it out internally.

The only caveat is that when text is rendered from a component to a view, it’s usually for simple objects like string or number. These data types obviously don’t have special functionality built in to notify when they have been updated.

In such cases, the responsibility falls to Angular itself to set up appropriate places where values within views can be updated as required. This is both complicated and fascinating to read about.

This all makes sense and works well for as long as we constrain ourselves to a single component. But the moment we add another component, and want to pass a variable into that component, the complexity is kicked up a notch.

How do we handle changes between bound data that occur in the child component? Let’s use a sample app to demonstrate the problem, and how signals in our components can help.

Building a price tracker app to demonstrate Angular signals

Let's imagine we have an app that’s tracking the price of four different products. Over time, the price of the product can go up or down. It’s rudimentary, but will help us to understand the concept at hand. It looks like this: Demo Cat Product Pricer Tracker App Showing Four Cat Related Products And Updating To Show Change In Price The data is provided through an interval that updates every second. It stays subscribed until the component is destroyed. Until then, it updates the model with new random price data:

ngOnInit() {
  this.timerSub = interval(1000)
    .pipe(takeUntil(this.$destroying))
    .subscribe(x => {
    this.model = [
      {
        name: "The book of cat photos",
        price: getRandomArbitrary(5, 15)
      },
      {
        name: "How to pat a cat",
        price: getRandomArbitrary(10, 40)
      },
      {
        name: "Cat Photography: A Short Guide",
        price: getRandomArbitrary(12, 20),
      },
      {
        name: "Understanding Cat Body Language: A Cautionary Tale",
        price: getRandomArbitrary(2, 15)
      }
    ]
  });
}
Enter fullscreen mode Exit fullscreen mode

Next up, we also have our ChildComponent which shows the list of prices. It just accepts an Input() of type Array<PriceData>. Every second, the price data updates, and the update flows to our child component. Nice.

Reacting to data changes with ngOnChanges in Angular

But now, we want to introduce an improvement. When the price goes up or down for individual items, we want to visually signify that to the user. Additionally, how much the product has gone up or down by should show.

Essentially, we are reacting to changes in the data. Before signal inputs, we’d have to implement the OnChanges interface in our component. Let’s go ahead and bring that in now:

export class ChildComponentComponent implements OnChanges {
  ngOnChanges(changes: SimpleChanges): void {
    console.log(changes);
  }
  @Input() data: Array<PriceData> | undefined;
}
Enter fullscreen mode Exit fullscreen mode

Now we get notified each time the data has changed, and the output is logged. Let’s see how that helps us. Our console window can give us more insight: Console Window Showing Data Change With Output Logged First up, our data changes from undefined (previousValue) to the new value (currentValue): Example Showing Data Changing From Undefined To New Value On subsequent changes, the old data is updated to the new data. This repeats every time the value is changed on the component.

There’s nothing technically wrong with this approach. But in Angular, with TypeScript, whose main selling point is types, there’s certainly a lack of types being handed around. The types of previousValue and currentValue are just any: Example Showing Lack Of Types In Angular Project For Previousvalue And Currentvalue Variables To meet our requirements, this means we have to blindly cast from these types into types that we expect before we can work on the data. Our ngOnChanges becomes the following: Using Ngonchanges To Cast Expected Types We likely started our Angular project with high hopes of using types, but this code almost immediately feels like a gutterball for two main reasons:

  1. We use an index signature to access the data object, which we hope we haven’t typed or entered incorrectly, because there’s nothing saving us from that situation
  2. We shove previousValue and currentValue into their respective types, with no idea as to how the implementor is populating these values. If we refactor the code tomorrow and change the type that comes into the component via the Input() directive, our code will stop working and we wouldn’t be sure why

Remember, this is in a simple application as well. If we were working on an app with any more complexity, it’s not hard to see how using ngOnChanges would become unwieldy.

We could introduce some techniques to help deal with it, but in reality, the changes coming into our component probably should have some sort of type, and should react appropriately when they are updated. Fortunately, that’s exactly what signals do.

Signals to the rescue in our Angular demo

Signals, introduced recently in Angular, can help us remove our dependency on ngOnChanges, and make it easier for us to achieve a better solution. Admittedly, bringing signals into this code does require a bit of reasoning, but leaves us with cleaner code that makes more sense.

If we were to break down what’s happening here in plain English, the description of the problem would be:

  • We receive a list of prices
  • When the prices change, we want to store the received prices in an “old prices” variable
  • Then, we want to compare the new prices with the old prices

This helps us understand two key components to how we’ll solve this with signals.

First, using “when the x happens” language indicates that we’ll need to use an effect because we want something to happen when the signal changes — in this case, storing the old value to a variable.

Second, using a phrase like “and then compare” indicates that we want to compute a value that depends on the incoming value. Unsurprisingly, this means we’ll need to use a compute function.

Okay, let’s bring these changes into our component. First of all, we’ll need to remove the dependency we have on ngOnChanges, as that’s no longer a dependency of this change detection. Next, we’ll need some new properties for the data:

prices = input.required<Array<PriceData>>(); // The incoming price data
oldPrices = Array<PriceData>();
Enter fullscreen mode Exit fullscreen mode

Creating the effect

Ah, this is the easy part. Basically, whenever the prices update, we just want to store the last emitted value into an oldPrices variable. This happens in our constructor:

constructor() {
  effect(() => {
    this.oldPrices = this.prices();
  });
}
Enter fullscreen mode Exit fullscreen mode

Admittedly, it still feels weird at times calling prices like it’s a function, but it’s how we interact with signals. We receive an array of prices, which are immediately set to the oldPrices variable.

But if we’re just doing this every single time the value changes, how will we effectively compare the old and new values? Simple — we have to compute it.

Creating the computed function

Within our computed function, we now have access to a fully type-safe instance of our prices and prices array. Whenever the prices signal changes, computed sees that the signal has changed, and updates the computed signals as required. The comparison occurs, and our new computed signal is returned:

priceDifferences = computed(() => {
    let priceDelta = [
      this.priceCompare(this.oldPrices[0], this.prices()[0]),
      this.priceCompare(this.oldPrices[1], this.prices()[1]),
      this.priceCompare(this.oldPrices[2], this.prices()[2]),
      this.priceCompare(this.oldPrices[3], this.prices()[3]),
    ]

    return priceDelta.map(x => ({
      change: x,
      direction: (x ?? 0) > 0 ? PriceDirection.Increasing : PriceDirection.Decreasing,

    } as PriceDescription));
})
Enter fullscreen mode Exit fullscreen mode

In our example, the computed function runs first, and then the effect function runs second. This means that the old and new values are stored and compared effectively.

It’s also worth mentioning that when I first wrote this code, I attempted to set a signal from the effect code and skip the computed signal altogether. That’s actually the wrong thing to do — and Angular won’t let you do it unless you change a setting — for a couple of reasons:

  1. Updating signals from within effects makes it difficult to track what is updating and why
  2. Signals are mutable and can be set by you, whereas computed signals are read-only — they can’t be set by you. This makes sense when your computed signal is downstream from your other data

The benefits of this approach is that our code has more type safety, and it makes more sense to read and understand. It also means that our components will work if our change detection is set to OnPush, and sets us up for Angular’s move away from using zones for change detection.

The other nice thing about this approach is that it actually solves a problem that a lot of Angular developers will probably have in the future.

Namely, with no ngOnChanges giving old and new values to identify what’s changed, how will we perform comparisons? Fortunately, it’s as easy as setting up an effect to store the old value, and then performing the comparison in a computed signal value.

Conclusion

Angular is evolving in some pretty exciting ways. In this tutorial, we explored how signals are growing in Angular to enhance state management.

To see how to use signals for better state management in Angular, we created a demo project and looked at the “old” approach using ngOnChanges as well as the improved approach using signals.

As always, you can clone the code yourself from this GitHub repo. You can use the commit history to change between a version of the app with ngOnChanges and the newer Signals implementation.


Experience your Angular apps exactly how a user does

Debugging Angular applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Angular state and actions for all of your users in production, try LogRocket.

LogRocket Signup

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your site including network requests, JavaScript errors, and much more. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred.

The LogRocket NgRx plugin logs Angular state and actions to the LogRocket console, giving you context around what led to an error, and what state the application was in when an issue occurred.

Modernize how you debug your Angular apps — start monitoring for free.

Top comments (1)

Collapse
 
fyodorio profile image
Fyodor

Honestly, these new contraptions from Angular guys are bad… Their paradigm was complex but consistent and logical before. Now it’s just complex. Kinda like lavender latte and stuff… Makes no sense at all…