avatar🪄 OZ 🎩

Summary

Angular 17.next.8+ introduces a breaking change by replacing the default equality check function in signals to Object.is(), necessitating updates in projects using Angular Signals to ensure proper UI updates and performance.

Abstract

With the release of Angular 17.0.0-next.8, a significant update to Angular Signals has been introduced that affects the default equality check function. This function now defaults to Object.is(), which compares objects by reference rather than by value. This change means that developers must ensure that objects used in signal.set(), signal.update(), and computed() are new instances to trigger UI updates, as mutating the same object will no longer suffice. The article provides a guide for developers to update their existing Angular Signals projects, recommending the adoption of immutable structures, the use of slice() for arrays, and {…object} for objects where necessary. For cases where immutability is too costly, custom equality check functions can be implemented. The article also advises on handling signal.mutate(), computed(), and toSignal() in light of the new equality check behavior, and provides the code for the previous default equality check function for reference.

Opinions

  • The author suggests that while immutable structures are recommended by default, it's acceptable to use mutable structures if immutability proves too expensive for certain cases.
  • The author implies that the new equality check function, Object.is(), is more efficient but requires developers to be more explicit about object (im)mutability.
  • There is an acknowledgment that some structures are costly to re-instantiate, indicating an understanding of the potential performance implications of the change.
  • The author encourages sharing knowledge by sharing the article and subscribing for more insights, indicating a commitment to community-driven knowledge exchange.

New Equality Check Function in Angular Signals

If you have a project that already uses Angular Signals, this article might help you to update your project to Angular v17.next.8+.

“Matinée in Arcachon”, Pierre Bonnard, 1930

The main breaking change of Angular 17.0.0-next.8 release is not even mentioned in the changelog: the default equality check function in signals has been replaced, and now it’s just Object.is().

The previous function would consider any two objects as non-equal, so if in signal.set() you use objects, and your signal.update() or computed() return objects — you would always receive a notification, and the UI would be always updated.

Object.is() compares objects by reference, so if you return the same object, just mutated, or if signal.set() receive the same object, just mutated, your signal will not send a notification.

For example, if your signal contains an instance of Map, and in update() you just want to assign a new key — it will not trigger an update now, you need to re-instantiate a map (as you do with immutable structures), or use a custom equality check function.

Some structures are relatively small and cheap to re-instantiate, and some are not. I recommend you use immutable structures by default, but if you have some cases where it’s just too expensive — I will not say a word against it.

So you have an existing project that uses Angular Signals. From my experience, I recommend the next steps.

Signals

  1. Find every call of signal() (the function that creates signals).
  2. You can safely skip signals that contain primitive values.
  3. Check every set() call for the remaining signals — you might want to add slice() for arrays or {…object} for objects if you can not be sure that you receive a new instance in every call. Or, you can add a second argument to signal(), where you can provide the equality check function.

update()

  1. Find every call of update() on your signals.
  2. Skip signals that contain primitive values or have custom equality check functions (set in the previous step).
  3. Check functions provided toupdate() — if they return newly created objects or arrays, or they create new arrays using Array.map(), Array.filter(), Array.slice() — you can safely skip them.
  4. For the remaining cases, decide what is better for you — either return a new object/array, or add a custom equality check function to the signal() call where you create that signal.

mutate()

If you were using signal.mutate() — find and replace it with update(). You can apply the steps I provided update() above.

computed()

  1. Find every call of computed().
  2. Check functions provided to computed() — you can skip those that return primitive values or new instances of objects/arrays.
  3. For the remaining cases, decide what is better — return a new instance or set a custom equality check function for the given computed().

toSignal()

  1. Find usages of toSignal().
  2. You can skip usages where the source observable emits primitive values or you know that every produced value is a new instance of an object or array.
  3. For the remaining cases, you can add the map() operator to the source observable if you want the resulting signal to always notify its consumer. In that map() function, you’ll need to create a shallow copy of an object or array. Maps and sets will need special handling if you want them to remain maps and sets.

If you are looking for the code of the previous default equality check function, here it is:

export function equalPrimitives<T>(a: T, b: T): boolean {
  return (a === null || typeof a !== 'object') && Object.is(a, b);
}

It compares all non-null and non-object types using Object.is(), but objects are always non-equal.

🎙 All my articles are free and only exist to share knowledge. If you think this knowledge is worth sharing, please share the link! 🤝

💙 If you enjoy my articles, consider subscribing to receive my new articles by email (“following” on Medium will not notify you about new articles).

Angular
Recommended from ReadMedium