Table of Contents

  1. What are signals ?
  2. Why providing an equality function can be handful
  3. Different ways to write an equality function

What are signals ?

Signals are a new reactive type introduced in Angular 16. They are similar to observables but offer a (maybe) simpler way to manage reactive data in your Angular applications.

Signals provide a declarative syntax to manage our states and components, which I like and used a lot in these past few years.

Here is a basic example:

const counter = signal(0);

function increment() {
  counter.update((prev) => prev + 1);
}

Obviously the topic is really wide and I won't explain here every aspect of a signal. If you want to have a better understanding and a full picture of this topic, you can check the Angular documentation, which provides a full guide with great information and examples.

Why providing an equality function can be handful

By default, Angular Signals use a referential equality (Object.is()) to determine if two values are equal.

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.

const data = signal(['test']);
data.set(['test']); // We are passing in a new array, so the signal is considered as changed 

When creating a signal, you can optionally provide an equality function, which will be used to check whether the new value is actually different than the previous one.

import _ from 'lodash';
const data = signal(['test'], {equal: _.isEqual});
data.set(['test']); // Won't notify a change

Lodash equality function performs a deep check on the compared objects:

import _ from 'lodash';

Object.is(['test', 'test']); // false
_.isEqual(['test', 'test']); // true

It can be helpful when an other signal depends ono the previous one, for example with a computed signal:

const data = signal([
    { type: 'animal', name: 'cat' },
    { type: 'animal', name: 'dog' },
    { type: 'object', name: 'book' },
]);

// Computed signal that will be re-computed each time data changes
const animals = computed(() => {
    console.log('I am being computed');
    return data().filter(el => el.type === 'animal');
});

// Setting new value in data, but with same elements
data.set([
    { type: 'animal', name: 'cat' },
    { type: 'animal', name: 'dog' },
    { type: 'object', name: 'book' },
]);

// Without equality function:
// -> I am being computed
// -> I am being computed

// With equality function (_.isEqual for instance)
// -> I am being computed

Different ways to write an equality function

  1. Lodash isEqual

As seen in the previous example, lodash provides a isEqual() method to determine deep equality between two objects. Using the isEqual() method from this library, we can perform a deep comparison between the given operands. It will return a Boolean value indicating whether the operands are equal based on JavaScript strict equality (===) on all attributes of the two given objects.

  1. JSON.stringify

This method is more of a trick that we can use to determine whether two objects are deep equal or not. Even though JavaScript does not have an out-of-the-box solution to compare two objects, it has no problem comparing two strings.

const user1 = {
    "firstName": "Harry",
    "lastName": "Potter",
    "age": 11 
}

const user2 = {
    "firstName": "Harry",
    "lastName": "Potter",
    "age": 11 
}

user1 === user2; // false
JSON.stringify(user1) === JSON.stringify(user2); // true

However, it it important to notice that this method will fail if one of the compared objects contains a circular structure, for instance:

const obj = {};
obj.circular = obj;
const jsonString = JSON.stringify(obj); // Error occurs here

/** 
TypeError: Converting circular structure to JSON
    --> starting at object with constructor 'Object'
    --- property 'circular' closes the circle
at JSON.stringify (<anonymous>)
* /

Although this way of comparing objects is really simple, be careful on when to use it.

  1. Manual comparison

You can also implement the comparison yourself, for example with something like that:

function isDeepEqual(object1, object2) {

  const objKeys1 = Object.keys(object1);
  const objKeys2 = Object.keys(object2);

  if (objKeys1.length !== objKeys2.length) return false;

  for (var key of objKeys1) {
    const value1 = object1[key];
    const value2 = object2[key];

    const isObjects = isObject(value1) && isObject(value2);

    if ((isObjects && !isDeepEqual(value1, value2)) ||
      (!isObjects && value1 !== value2)
    ) {
      return false;
    }
  }
  return true;
};

function isObject(object) {
  return object != null && typeof object === "object";
};

isDeepEqual(user1, user2); // true
  1. Specific libraries / frameworks methods
  • deep-equal
const deepEqual = require('deep-equal');
deepEqual(user1, user2, {strict: true}); // true
  • Node.js
const assert = require('assert');
assert.deepStrictEqual(user1, user2); //true

So which one should you use?

In my opinion, none of the above method is the "perfect one", so it really depends on what you are looking for: is it performance ? simplicity of use ? fine grain control on what's going on ? Etc. The answer may also vary depending on the specific scenario you have to handle.

In a future post, I will benchmark performance of those different methods.

To be continued :)