Efficient Patterns for Effects
Tags: typescript, angular, javascript, webdev
Published: 2024-04-17 04:54:06

Angular's signals have been quite a revolution since they came out, from [new state management approach](https://ngrx.io/guide/signals/signal-store) to a brand new way of rethinking reactivity within applications, signals have deeply influenced the framework's ecosystem.

While most of its primitives are stable, `effect` is still in developer preview as of the time I'm writing this article, and can often lead to errors or warnings.

In this post, we will cover the basics of the usage of `effect`, some tips and tricks I use, and showcase a way to enhance their behavior.

## Effect in a Nutshell

Effect is a function that defines a callback to be called whenever any of the signal it wrapped changes:

```ts
const age = signal(26);

effect(() => {
  console.log(`Happy ${age()}th birthday! 🎂`);
});

age.update(age => ++age);
// -> Happy 27th birthday! 🎂
```

## Tips and Tricks

The core of what compose an effect is there, but in the real world, they can prove harder to use and to maintain, leading to unexpected behaviors or bugs.

After heavily using them, the Angular's community found patterns and ways to ease their usage, here are some I found particularly useful.

> 📝 This is just tips *I* enjoy using, they don't necessarily come from any sort of official guideline or convention.

### Explicit your dependencies

In the previous example, seeing that the effect relies on `age` is fairly easy to spot since there is only a single line and a single signal.

In a larger codebase, it might be a bit harder, or simply add some unecessary cognitive load.

To both simplify the reading of an effect, and clarify its dependencies, it is common to see the first lines of a signal explicitely unwrapping the values:

```ts
effect(() => {
  // 👇 Indicate which signals this effect will work on
  const productId = productId();
  const isLoggedIn = isLoggedIn();

  // Logic based on the unwrapped values
});
```

### Small and Focused Effects

When using them, try to write Small and Focuses Effects (stay SaFE!).

If they grow, effects will be unclear as what their initial goal was, and if you need to consume one more signal in the future, it might have some unwanted side effects.

Keeping the effects concise clarify their scope and make them easier to read:

```ts
// ❌
effect(() => {
  const age = age();
  const isLoggedIn = isLoggedIn();
  const selectedProductId = selectedProductId();
 
  // ...
});

// ✅
effect(() => {
  const isLoggedIn = isLoggedIn();

  // ...
});

effect(() => {
  const age = age();
  const selectedProductId = selectedProductId();
 
  // ...
});
```

### Name your things

Since effects are "just" callbacks, it can sometimes prove difficult to understand their meaning.

However, we can take advantage of its API to name the `EffectRef` it returns, and hence explicit its goal.

For example, here is the previous example:

```ts
const redirectAnonymousEffect = effect(() => {
  const isLoggedIn = isLoggedIn();

  // ...
});

const ensureLegalAgeEffect = effect(() => {
  const age = age();
  const selectedProductId = selectedProductId();
 
  // ...
});
```

> 📝 Notice that by naming things, you might also notice that an effect tries to englobe too many things and needs to be split in parts.

### Untrack what doesn't need to be tracked

Since effects tracks signals called from a [reactive context](https://www.rx-angular.io/docs/template/concepts/reactive-context), it can help to go a step further than just declaring the dependencies by plainly untrack the rest of the logic:

```ts
const ensureLegalAgeEffect = effect(() => {
  const age = age();
  const selectedProductId = selectedProductId();
 
  // 👇 Nothing in here can impact the trigger of the effect
  untracked(() => {
    // ...
  });
});
```

While it's a bit more verbose, it's a convenient way to ensure that the logic will not impact the effect's trigger.

## Supercharging effects

Keeping those tips in mind is great, applying them even more, but staying consistant relying only on self discipline can be hard, even more in a team.

However, with a bit of TS magic, GitHub comments and time, we can write a wrapped function for those principles:

{% embed https://gist.github.com/pBouillon/b51c426bc7387ca177850bf628f2fa43 %}

Its usage allows you to specify the signals to listen to, and a function to be executed whenever the dependencies changed. Any signal called within it won't mark it as a dependency of the effect:

```ts
const loginChangedEffect = effectFromDeps(
  [this.loginStatus],
  ([loginStatus]) => {
    console.log(`[${loginStatus}] There is currently ${this.userCount()} users`)
  });
```

By providing an additional option object, we can actually go a step further and have finer control on the effect's lifecycle:

{% embed https://gist.github.com/pBouillon/344bf1ed92504292290b52ed2d914a0d %}

This allows us to specify callbacks on effect's creation and cleanup:

```ts
const loginChangedEffect = effectFromDepsWithLifecycle(
  [this.loginStatus],
  ([loginStatus]) => {
    console.log(`[${loginStatus}] There is currently ${this.userCount()} users`)
  }, {
    onCreation: () => console.log('Created'),
    onCleanup: () => console.log('Cleaned up'),
  });
```
That’s all I had to share today, happy coding!