Megan Lee
Posted on June 13, 2024
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: 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)
}
]
});
}
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;
}
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: First up, our data changes from undefined (previousValue
) to the new value (currentValue
): 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
: 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: 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:
- 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
- We shove
previousValue
andcurrentValue
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 theInput()
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>();
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();
});
}
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));
})
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:
- Updating signals from within effects makes it difficult to track what is updating and why
- Signals are mutable and can be set by you, whereas
computed
signals are read-only — they can’t beset
by you. This makes sense when yourcomputed
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 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.
Posted on June 13, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.