DEV Community

Cover image for Angular Inputs and Single Source of Truth
Evgeniy OZ
Evgeniy OZ

Posted on • Originally published at Medium

Angular Inputs and Single Source of Truth

The main way for Angular components to receive external data is through “inputs.” In this article, I’ll explain why the Single Source of Truth principle should be applied to inputs.

Wikipedia provides a scientific description of the Single Source of Truth, but if we apply it to Angular components and their inputs, it will sound like this:

Inputs of a component should only be modified from outside the component.

This simple rule can save your code from very weird bugs. Let’s explore the simplest case:

In this small demo (source code), you can add items to your shopping cart, but after 10 seconds, the list will be replaced with some items you didn’t pick.

This demo emulates a situation where the shopping cart data is loaded from the server, but while the request was loading, the user was able to add some items to the cart. Maybe it’s not the best demo, but I hope it illustrates the idea. Creating code with bugs intentionally is not as easy as it happens unintentionally 😉

Generally speaking, the state of our component is globally mutable. We should avoid it everywhere, not only in components. But let’s see how we can avoid it in components.

Immutable Inputs

Thankfully, Angular gives us two ways to create inputs, that can not be modified inside the component: accessors and signals.

In Angular before v17 accessors were the only way:

    @Component()
    export class ShoppingCart {
      private _items: Item[] = [];

      @Input() set items(items: Item[]) {
        this._items = items;
      }

      get items() {
        return this._items;
      }
    }
Enter fullscreen mode Exit fullscreen mode

In Angular v17.1 we got Signal Inputs:

    @Component()
    export class ShoppingCart {
      items = input<Item[]>([]);
    }
Enter fullscreen mode Exit fullscreen mode

Fewer lines, more awesomeness.

There are also more practical reasons to use signal inputs over setters, you can read about them in this article by Matthieu Riegler.

Now if we try to update items inside the component:

    @Component()
    export class ShoppingCart {
      items = input<Item[]>([]);

      private readonly api = inject(Api);

      constructor() {
        this.api.getCartItems().subscribe((items) => {
          this.items.set(items);
          //         ^^:
          // 🔴 Property 'set' does not exist on type 'InputSignal<Item[]>'.
        });
      }
    }
Enter fullscreen mode Exit fullscreen mode

TypeScript generates an error and protects our code.

If you encounter an error like this, do not try to find a workaround — Angular and TypeScript are on your side here and trying to help you. Modify your code to fix the bug. Sometimes it will require effort and time, but you’ll fix one of the nastiest kinds of bugs.

To fix our example, we should decide which part of our code should be the source of truth for the list of items in our shopping cart. Only that code will be allowed to modify (mutate) the list of items. Also, this code should be responsible for protecting the list from undesired modifications.

In our example, the root component doesn’t need to know what items are in the shopping cart, so it should either the Api service (which is shared globally), or the ShoppingCart component.

In real apps, we might need information about the shopping cart items not only in the ShoppingCart, but also in the navigation components.

So, let’s make the Api service the source of truth for this list:

Now, if you explore the source code, you’ll find that most of the logic is moved to the Api service, and this service now has the only source of truth for the shopping cart items. And now, this service protects this source of truth from unintentional modifications.

      addItemToCart(item: Item) {
        if (untracked(this.$isLoading)) {
          return;
        }
        this._$cartItems.update((items) => Array.from(new Set([...items, item])));
      }
Enter fullscreen mode Exit fullscreen mode

If you are curious why untracked() is used here, this article will be interesting for you.

The code of our components becomes much more compact, and the ShoppingCart component is now a “dumb” component.

    export class ShoppingCart {
      items = input<Item[]>([]);
      removeItem = output<Item>();

      protected remove(item: Item) {
        this.removeItem.emit(item);
      }
    }
Enter fullscreen mode Exit fullscreen mode

The Api service has a little more code now, but for a very good reason — we’ve fixed a bug!

This article uses new features of Angular v17.1, but you could achieve it with just accessors and observables in any version of Angular. Code examples are far from perfect, and it was done intentionally to illustrate how a bug can be introduced and fixed.

The “Signal Source of Truth” architecture (or “rule,” or “principle”) states that if some information is shared across the application, there should be only one part of the code that can mutate this information. This principle can protect your application from bugs, which, as is often the case in practice, are much worse than theoretical examples in articles. Apply it to your components and services to make them more robust!

🪽 Do you like this article? Share it and let it fly! 🛸

💙 If you enjoy my articles, consider following me on Twitter, and/or subscribing to receive my new articles by email.

Top comments (1)

Collapse
 
jangelodev profile image
João Angelo

Hi Evgeniy OZ,
Your tips are very useful
Thanks for sharing