Understanding Signals and Effects in Angular
Tags: angular, webdev, javascript
Published: 2024-05-30 13:56:56

In Angular development, understanding signals and effects is pivotal for managing state and handling asynchronous operations effectively. Signals serve as wrappers around values, while effects are operations triggered by changes in these signals. Let’s delve into these concepts and explore their practical applications.

## What are signals?

A **signal** in Angular is a wrapper around a value that notifies its consumers when the value changes.

**Signals** can be either **writable** or **read-only** and can contain any type of value, from primitives to complex objects.

## 1. Writable signals

Writable signals are simple, direct holders of a value. When you create a writable signal, its value is immediately available, and you can access and modify it directly at any time.

**Creating a Writable Signal:**

```
const count = signal(0);
console.log('The count is: ' + count());
```
**Setting a New Value:**

```
count.set(3);
```
Updating the Value:

```
count.update(value => value + 1);
```

## 2. Computed signals

Computed signals derive their values from other signals and automatically update when those signals change. They are read-only which means you cannot directly assign values to a computed signal.

They are defined using the computed function.

**Creating a Computed Signal:**

```
const count = signal(0);
const doubleCount = computed(() => count() * 2);
```
**Lazy Evaluation:** The computation for doubleCount doesn’t run until doubleCount is accessed for the first time.

**Memoization:** The calculated value is cached. If the source signals change, the cached value is invalidated, and the new value is recalculated when next read.

## Effects

An **effect** is an operation that runs whenever one or more signal values change.

**Effects** run at least once and re-run whenever the **signals** they depend on change.

Effects always execute **asynchronously**, during the change detection process.

## Creating an Effect

```
const count = signal(0);

effect(() => {
  console.log(`The current count is: ${count()}`);
});

// Outputs: "The current count is: 0"
count.set(2); // Outputs: "The current count is: 2"
```
**Use cases for effects**

Effects can be useful for:

- Logging data changes
- Syncing data with local storage
- Custom DOM behavior (e.g., changing background color based on 
  State)
- Custom rendering (e.g., drawing on a canvas whenever a signal 
  changes)

Using effects for state propagation can result in **ExpressionChangedAfterItHasBeenChecked** errors, infinite circular updates, or unnecessary change detection cycles.

Angular by default prevents you from setting **signals** in **effects**. It can be enabled if absolutely necessary by setting the **allowSignalWrites** flag when you create an **effect**.

```
const count = signal(0);
const doubleCount = signal(0);

effect(() => {
  doubleCount.set(count() * 2);
}, { allowSignalWrites: true });

count.set(1);  // This will now work but use cautiously
console.log(doubleCount());  // Outputs: 2
```
However instead of using **effects** for state propagation, it’s better to utilize **computed signals** to model state that depends on other state. This approach can help keep your application more predictable and manageable.

**Effect Execution Context**

By default, you can **only create an effect()** within an **injection context** (where you have access to the inject function) such as a component, directive, or service constructor.

To create an effect outside the constructor, you need to pass an Injector instance to the effect via its options. Passing an **Injector** instance to an effect’s options when creating it outside the constructor ensures that the effect has access to Angular’s dependency injection system.

```
@Component({...})
export class EffectiveCounterComponent {
  readonly count = signal(0);
  constructor(private injector: Injector) {}
  initializeLogging(): void {
    effect(() => {
      console.log(`The count is: ${this.count()}`);
    }, {injector: this.injector});
  }
}
```

## Destroying Effects

Effects are automatically destroyed when their enclosing context (such as a component, directive, or service) is destroyed.

They can also be manually destroyed using the the **EffectRef’s .destroy()** method. You can combine this with the **manualCleanup** option to create an effect that lasts until it is manually destroyed.

```
export class DoubleCounterComponent {
  readonly count = signal(0);
  readonly doubleCount = computed(() => this.count() * 2);

  private doubleCountEffect: EffectRef;

  constructor() {
    this.doubleCountEffect = effect(() => {
      console.log(`The double of count is: ${this.doubleCount()}`);
    }, { manualCleanup: true });
  }

  destroyEffect() {
    this.doubleCountEffect.destroy();
  }
}
```

## Reading Without Tracking Dependencies

Sometimes, you may want to read a signal without creating a dependency. This can be done using **untracked**.

```
effect(() => {
  console.log(`User set to ${currentUser()} and the counter is ${untracked(counter)}`);
});
```
**untracked** is also useful when an effect needs to invoke some external code which shouldn’t be treated as a dependency:

```
effect(() => {
  const user = currentUser();
  untracked(() => {
    // If the `loggingService` reads signals, they won't be counted as
    // dependencies of this effect.
    this.loggingService.log(`User set to ${user}`);
  });
});
```
This approach ensures that your effects behave predictably and only re-run when you intend them to, avoiding unnecessary computations.

## Effect cleanup functions
Effects can register cleanup functions to handle things like canceling a timeout if the effect runs again or is destroyed.

This ensures the timer is cleared if the effect re-runs before the timeout completes.

```
effect((onCleanup) => {
  const timer = setTimeout(() => {
    console.log('Timer finished');
  }, 1000);
  onCleanup(() => {
    clearTimeout(timer);
  });
});
```

## Conclusion

In conclusion, signals and effects form essential constructs in Angular for managing state, handling asynchronous operations, and ensuring efficient change detection. Understanding and leveraging these concepts appropriately contribute to building robust and scalable Angular applications.

## CTA
💛If you enjoy my articles, consider [subscribing to my newsletter](https://bytewave.substack.com/) to receive my new articles by email