Signals are a powerful tool in Angular to handle reactivity, but they can easily cause unnecessary updates, wasted requests, and performance issues if not carefully managed. In this article, we’ll explore how Signals emit updates, why equality checks (equal
) matter, and how to implement efficient deep comparison strategies to keep your applications smooth and reliable.
🔄 How Signals Emit Updates
Each time the reference changes, the Signal is considered “dirty”, and all derived Signals, the DOM, and effect
functions will also update.
When the signal return a string or a number, it compare the value using
===
(But this article is about dealing whith object returned by the signal)
🚫 Preventing Useless Updates with equal
To avoid unnecessary Signal pipeline updates, you can use the equal
option to manually define comparison logic.
This looks simple for shallow objects, but with deep objects the work becomes much heavier and harder to maintain.
📡 Angular Resource and Annoying Updates from the Source
The same issue exists with Angular Resource
. You may use a Signal as a source for the params
option.
This triggers an async call each time the Signal emits a new value. However, each new call cancels the previous one (similar to switchMap
in RxJS).
This not only causes unnecessary requests but also removes the previous value from the Resource
, which can lead to strange side effects like flaky page rendering.
⚖️ Performance of equal
with Deep Nested Objects
To prevent unnecessary updates, you could add manual equality checks before calling mySignal.set(...)
or mySignal.update(...)
.
But this is easy to forget. That’s why I prefer using the equal
option:
signal({ id: 1 }, { equal: (a, b) => a.id === b.id });
However, when dealing with deeply nested objects, equality checks can become expensive. Andrew Jarrett explored the fastest JavaScript solution for deep equals comparison, which might be helpful.
🚀 How to Make JavaScript’s Fastest Deep Equal Comparison
Here’s his article: How to build JavaScript's fastest “deep equals” function.
The solution looks like this:
const addressEquals = Function(
"x",
"y",
`
if (x === y) return true
if (x.street1 !== y.street1) return false
if (x.street2 !== y.street2) return false
if (x.city !== y.city) return false
return true
`
);
This solution consists of wrapping your comparaison inside a string and evaluate it with the constructor of the Function.
This approach is fast but not type-safe. If your object’s shape changes, you could forget to update the comparison string.
But even if it is not typesafe, I think, it is possible to create a dedicated Type test and using a custom Type mapper to transform the object into a target string with the same shape of your object
interface MyObject {...};
const MyObjectComparaisonString = `...` as const
type IsMyDeepEqualsStringCorrect = Expect<Equal<ToDeepEqualsComparaion<MyObjectType>, typeof MyObjectComparaisonString >>
This is just an idea, but it can be enough for your needs.
But if you already use a schema library, this lib is a perfect for your needs !
📦 @traversable
: A Simple Solution for Deep Equality with Schema Validators
Andrew Jarrett also created a library that makes deep equality trivial when working with schema validators.
Using zod
as an example:
import { z } from "zod";
import { zx } from "@traversable/zod";
const Address = z.object({
street1: z.string(),
street2: z.optional(z.string()),
city: z.string(),
});
// Create the deep equal function 👇
const addressEquals = zx.deepEqual(Address);
addressEquals(
{ street1: "221B Baker St", city: "London" },
{ street1: "221B Baker St", city: "London" }
); // => true
When used with a Signal:
const address = signal(..., { equal: addressEquals });
const streetViewFromAddressResource = resource({ params: address, ... });
Now streetViewFromAddressResource
is only called when the address really changes.
This works with multiple schema libraries:
- Zod
- JSON Schema
- ArkType
- TypeBox
- Valibot
It’s a clean solution that evolves with your schema without manual updates.
Note: Be aware that if your object/schema contains functions or dates, the way they are handled may be uncertain.
👉 If you found this article useful, don’t hesitate to leave a 👍 or share your thoughts in the comments — I’d love to hear your feedback!
If you don’t know me, I’m Romain Geffrault, and I regularly share Angular/TypeScript/RxJs/Signal content. Check out my other articles and follow me on LinkedIn Romain Geffrault
Top comments (2)
Wow, thank you for sharing my library! This is exactly the use case I had in mind when I created it: preventing unnecessary state updates :)
I can answer that! Here's how
zx.deepEqual
handles dates and functions.I also created a StackBlitz so your audience has something to play with.