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+.

🪄 OZ 🎩
3 min readOct 12, 2023
“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.

--

--