DEV Community

Cover image for Efficient Patterns for Effects
Pierre Bouillon for This is Angular

Posted on • Edited on

Efficient Patterns for Effects

Angular's signals have been quite a revolution since they came out, from new state management approach 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:

const age = signal(26);

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

age.update(age => ++age);
// -> Happy 27th birthday! 🎂
Enter fullscreen mode Exit fullscreen mode

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:

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

  // Logic based on the unwrapped values
});
Enter fullscreen mode Exit fullscreen mode

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:

// ❌
effect(() => {
  const age = age();
  const isLoggedIn = isLoggedIn();
  const selectedProductId = selectedProductId();

  // ...
});

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

  // ...
});

effect(() => {
  const age = age();
  const selectedProductId = selectedProductId();

  // ...
});
Enter fullscreen mode Exit fullscreen mode

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:

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

  // ...
});

const ensureLegalAgeEffect = effect(() => {
  const age = age();
  const selectedProductId = selectedProductId();

  // ...
});
Enter fullscreen mode Exit fullscreen mode

📝 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, it can help to go a step further than just declaring the dependencies by plainly untrack the rest of the logic:

const ensureLegalAgeEffect = effect(() => {
  const age = age();
  const selectedProductId = selectedProductId();

  // 👇 Nothing in here can impact the trigger of the effect
  untracked(() => {
    // ...
  });
});
Enter fullscreen mode Exit fullscreen mode

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:

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:

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

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

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

const loginChangedEffect = effectFromDepsWithLifecycle(
  [this.loginStatus],
  ([loginStatus]) => {
    console.log(`[${loginStatus}] There is currently ${this.userCount()} users`)
  }, {
    onCreation: () => console.log('Created'),
    onCleanup: () => console.log('Cleaned up'),
  });
Enter fullscreen mode Exit fullscreen mode

That’s all I had to share today, happy coding!

Top comments (2)

Collapse
 
nvdweem profile image
Niels van de Weem • Edited

In the supercharging part: Don't you have to do the const values within the effect callback? You won't actually have dependencies this way, right?

Collapse
 
pbouillon profile image
Pierre Bouillon

Nice spot, this is absolutely true and I misplaced that line, thanks for letting me know I have fixed it 🙌