Signals: the Do-s and the Don't-s
Tags: angular, rxjs, typescript, javascript
Published: 2024-02-06 09:43:10

_Original cover photo by [Shyam](https://unsplash.com/@thezenoeffect) on Unsplash._

If we ask any Angular developer "What is the hottest topic in Angular right now?", with almost 100% certainty everyone would respond "Signals!". And they would be right. Signals *are* the hottest topic in Angular right now. 

Of course, there is now a multitude of articles, videos, blog posts and so on, describing in deep detail what signals are, how they work, and different use cases, so we can safely assume almost everyone in the Angular community now knows about signals, and maybe at least has tried them out. But a fact that became quite obvious recently is that the community needs a specific set of rules for working with them, a set of Do-s and Don't-s, if you will. In today's article, let's dive into the world of signals, and find out examples of use cases where using signals *in a specific* way is not a good idea, and see what we should do instead. Let's get started!

### **Don't** use setters for inputs to turn them into signals

When signals first dropped in v16, support for lots of features was limited (as signals themselves were experimental). For instance, it was impossible to use signals as inputs to components. So, the community came up with a workaround: using setters to turn inputs into signals. The idea is simple: we create a setter for an input and a separate signal, and inside the setter we set that signal's value to be the value of the input. Then, we can use the signal as an input to the component. 

```ts
@Component({
  selector: 'app-my-component',
  template: `
    <div>
      {{ inputSignal() }}
    </div>
  `
})
export class SomeComponent {
  inputSignal = signal<string>();

  @Input() set input(value: string) {
    this.inputSignal.set(value);
  }

}
```

Obviously, this is quite verbose, especially if we have lots of inputs. This is really a lot of boilerplate, so let's see what we can do about it.

### **Do** use signal inputs

Starting from Angular v17.1, a new way of declaring input properties for components and directives has emerged: the `input` function. This function declares an input, but its value is a signal, rather than a plain property. We can simplify our example now:

```ts
@Component({
  selector: 'app-my-component',
  template: `
    <div>
      {{ inputSignal() }}
    </div>
  `
})
export class SomeComponent {
    inputSignal = input<string>('default value');
}
```
> Note: input signals are not stable as of v17.1, but will move to stable soon.

Of course, this is much, much better. This also supports all the features we have with regular inputs, for instance, to create a `required` input, we can do this:

```ts
export class SomeComponent {
  inputSignal = input.required<string>(); // we do not need to provide a default value here
}
```

We can also use transformers:

```ts
export class SomeComponent {
  booleanSignal = input(true, {transform: booleanAttribute});
}
```

And aliases:

```ts
export class SomeComponent {
  inputSignal = input.required({alias: 'condition'});
}
```

> Warning: signal inputs are read-only, it is not possible to set another value to it in the child component

Of course, the previous approach was a workaround for a missing feature, and with time all Angular developers will move to signal inputs. Now, let's focus on some more serious stuff, that can happen in any codebase.

### **Don't** retrieve data via HTTP calls in effects

This is a common mistake that can happen in any codebase. Let's say we have an input in our template, and we want to make an HTTP call when the input changes. We can do this:

```ts
@Component({
  selector: 'app-my-component',
  template: `
    <input (input)="query.set($event.target.value)" />
    <ul>
      @for (item of items) {
        <li>{{ item.name }}</li>
      }
    </ul>
  `
}) 
export class SomeComponent {
  query = signal<string>();
  items: Item[] = [];

    constructor(private http: HttpClient) {
        effect(() => {
            this.http.get<Item[]>(`/api/items?q=${this.query()}`).subscribe(items => {
                this.items = items;
            });
        });
    }
}
```

However, this poses several problems. First of all, the `items` collection is not a signal (we can make it into a signal, but then we would have to set `{allowSignalWrites: true}` on the effect, which is an even worse practice), which will make it hard to work with zoneless change detection in the future. Next, the declaration of `items` is separate from the place where its value is actually set, making it a bit harder to understand how it works; and finally, the triggering of the effect is decoupled from RxJS, meaning we cannot really harness the power of timing operators (like `debounceTime` in order to prevent unnecessary HTTP calls), as each change to the `query` signal will create a separate `Observable`. 

### **Do** use `toSignal` + `toObservable`

However, there is an easy way to circumvent all of those problems by utilizing the interoperability between signals and RxJS. Let's make a cleaner and more powerful way of doing this:

```ts
@Component({
  selector: 'app-my-component',
  template: `
    <input (input)="query.set($event.target.value)" />
    <ul>
      @for (item of items()) {
        <li>{{ item.name }}</li>
      }
    </ul>
  `
})
export class SomeComponent {
  http = inject(HttpClient);
  query = signal<string>();
  items = toSignal(
    toObservable(this.query).pipe(
      debounceTime(500),
      switchMap(query => this.http.get<Item[]>(`/api/items?q=${query}`))
    ),
  );
}
```

Here, we first move to the RxJS land with `toObservable`, make our asynchronous stuff there, and then move back to signals with `toSignal`. This way, we can use all the power of RxJS, and still have a signal in the end, and additionally, we solve the timing issue of multiple requests being made when the user types fast.

Let's move towards a more obscure example.

### **Don't** forget about `untracked`

Let's imagine we have a `CompanyDetailsComponent` page that displays company data, and a list of its employees, and allows general editing, for instance, we can change the description text of the company. There can be a lot of employees, so we want to be able to search through them with a query, as in the previous example. However, we are not searching through the entirety of employees, but only through the ones that are employed at this particular company, meaning that every time we search for an employee, we have to use the company ID too. We can do this:

```ts
@Component({
  selector: 'app-company-details',
  template: `
    <div>
      <h2>{{ company().title }}</h2>
      <input placeholder="Company description" 
      [ngModel]="company().description" 
      (ngModelChange)="updateCompanyDescription($event)" />
    </div>
    <input (input)="query.set($event.target.value)" />
    <ul>
      @for (employee of companyEmployees()) {
        <li>{{ item.name }}</li>
      }
    </ul>
  `
})
export class CompanyDetailsComponent {
  http = inject(HttpClient);
  query = signal<string>();
  company = input.required<Company>();
  employees = input.required<Employee>();
  companyEmployees = computed(() => {
    return this.employees().filter(employee => employee.companyId === this.company().id && employee.name.includes(this.query()));
  });
  @Output() companyUpdated = new EventEmitter<Company>();

  updateCompanyDescription(description: string) {
    this.companyUpdated.emit({
      ...this.company(),
      description
    });
  }
}
```

We can see that this example is somewhat complex. Let's break it down to simplify:

- The component receives the list of *all* employees and the company details
- We use a computed signal to get the list of employees working at this particular company and match the query
- We can update the company description, and emit an event when it happens so that the parent makes necessary HTTP requests
- We use the computed signal in the template

This will work just fine, however, we overlooked something: when the description gets updated, it is sent to the parent component, which will then send a new reference to the `company` signal input, and because it is also used in the computed `companyEmployees` signal, this will trigger the computation *unnecessarily*, as we know the `id` cannot change, and we don't really care about the `description`, or any other property for that matter. 

This can become even worse if we use an effect to trigger an HTTP call to make the search through employees, resulting in a lot of unnecessary HTTP calls every time the user types something in the unrelated `description` input.

### **Do** use `untracked`

Thankfully, this issue has a *very* simple solution: using the `untracked` function to read the signal's value instead of calling it the usual way. `untracked` will return the current value of the signal *without* marking it as a dependency of the current computed signal/effect, meaning that the computation will not be triggered when the value of that signal changes. Here we go:

```ts
companyEmployees = computed(() => {
  return this.employees().filter(employee => employee.companyId === untracked(this.company).id && employee.name.includes(this.query()));
});
```

> Note: in general, be careful with signals being called in the `computed` or `effect` function callbacks, rigorously check if they really need to be a dependency, and use `untracked` if they don't. 

Problem solved! Now, let's move to a really complex example, one that most people will overlook unless they are explicitly aware of this issue.

### **Don't** use `toSignal` in services to expose Observables as signals.

Lots of Angular applications do not use dedicated state management libraries like NgRx or Akita, and instead use services to manage the state of the application. This is a perfectly valid approach and can work really well. Most of such services use RxJS  `Observable`-s (more specifically `Subject`-s or `BehaviorSubject`-s) to propagate changes to the state throughout the application. Of course, now, it might be tempting to use `toSignal` to expose the `Observable` as a signal, so that the rest of the application can consume it directly. However, this can lead to a bunch of problems. One glaring one is that the signal will not be automatically disposed of when the component which uses it is destroyed, and the `Observable` will keep emitting values, which might not be the desired outcome. Also, `Observable`-s in general are way more versatile, and some components might choose to handle such values in a slightly different way, which will not be possible if the value is exposed as a signal. Let's view an example:

```ts
@Injectable({
  providedIn: 'root'
})
export class MessagesStore {
  private messagesService = inject(MessagesService);
  private messages$ = this.messagesService.getMessages();
  messages = toSignal(this.messages$);

  unreadMessages = computed(() => {
    return this.messages().filter(message => !message.read);
  });

  addMessage(message: Message) {
    this.messagesService.addMessage(message);
  }
}
```

In this scenario, we are reading messages in a live stream from some external service (FireBase, WebSocket, does not matter), and the source will connect to the service when the first subscriber appears, and disconnect when the last subscriber is gone. This is a perfect use case for an `Observable`, but not really for a signal. While we got some benefits like using `computed`, we also made sure that the connection starts as soon as the service is created, and does not really ever stop, even when the components that consume those signals are destroyed, as it is the `MessagesStore` that is the subscriber, and it will not be destroyed until the application is closed.

### **Do** use `toSignal` in components or expose state via connecting methods

So, what can we do about this problem? Well, one straightforward way of achieving this can be just not exposing signals from services at all and only call `toSignal` in components that consume the state. This way, we can be sure that the connection to the source will be established only when the component is created, and will be destroyed when all the consumer components are destroyed. This is a normal approach for most apps, however, it can be a bit tedious, as we might end up copy-pasting computed signals here and there (for instance, `unreadMessages` in the previous example). 

So, instead we can opt for using specific methods that "connect" a component to the `Observable`, while still returning a signal that lives in the component's injection context, and not the service's. Let's see how we can modify the previous example to use this approach:

```ts
@Injectable({
  providedIn: 'root'
})
export class MessagesStore {
  private messagesService = inject(MessagesService);
  private messages$ = this.messagesService.getMessages();

  addMessage(message: Message) {
    this.messagesService.addMessage(message);
  }

  messages() {
    return toSignal(this.messages$);
  }

  unreadMessages(messages: Signal<Message[]>) {
    return computed(() => {
      return messages().filter(message => !message.read);
    });
  }
}
```

This way, we use the `messages` method to actually get the overall messages, and in the case of other computed properties, we can use other methods that return computed properties. In the component, we can simply do this:

```ts
@Component({
  selector: 'app-messages',
  template: `
    <ul>
      @for (message of unreadMessages(messages())) {
        <li>{{ message.text }}</li>
      }
    </ul>
  `
})
export class MessagesComponent {
  private readonly messages = inject(MessagesStore);
  messages = this.messagesStore.messages();
  unreadMessages = this.messagesStore.unreadMessages(this.messages);
}
```

This way, we can ensure the stream we subscribe to in the component is safely disposed of when the component is gone. Of course, there are other ways of achieving this, but we list the simplest ones here. You can try and find other ways that best suit your project and codebase, but remember not to use the `toSignal` function in a shared service. 

### Conclusion

This article is meant to be a small ruleset for Angular developers when they deal with signals. Signals truly are exciting, and lots of teams now increasingly adopt them in their projects, but they can have caveats, and it is important to be careful about them. If you know more such issues, feel free to comment - let's see how we can solve them together! 

## Small promotion

As you might already know, I was very busy writing a book last year. It is called "Modern Angular" and it is a comprehensive guide to all the new amazing features we got in recent versions (v14-v17), including standalone, improved inputs, signals (of course!), better RxJS interoperability, SSR, and much more. If this interested you, you can find it [here](https://www.manning.com/books/modern-angular). The manuscript is already entirely finished, with some polishing left to do, so it is currently in Early Access, with the first 5 chapters already published, and more to drop soon. If you want to keep yourself updated on the progress, you can follow me on [Twitter](https://twitter.com/Armandotrue) or [LinkedIn](https://www.linkedin.com/in/armen-vardanyan-am/), where I will be posting whenever there are new chapters or promotions available.