10 Tips for Scaling Signals
Tags: angular, rxjs, signals
Published: 2023-09-07 09:52:24

---
title: 10 Tips for Scaling Signals
published: true
description: Signals scale very well if you follow the tips in this article. 
tags: angular,rxjs,signals
#cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bp4463ccbyghwkq4hk74.png
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zop9ocxg8w09a8jd1q31.png
# Use a ratio of 100:42 for best results.
# published_at: 2023-07-28 05:17 +0000
series: Signals vs RxJS
---

After converting ~1800 lines of code to signals, I have learned a few important lessons. Signals scale very well if you follow the tips in this article. 

## 1. `computed` can be inefficient by default

Edit: In the finalized version of signals, `computed` no longer runs excessively! [Skip to tip #2](#2-reuse-logic-not-state)

How many times will this log?

```typescript
export class Component {
  e = signal({ f: { f: 'f' } });
  f = computed(() => this.e().f);

  constructor() {
    effect(() => {
      console.log(this.f());
    });
    setTimeout(() => {
      this.child.c.update((e) => ({ ...e }));
    }, 1000)
  }
}
```

Well, the top-level object changes, but the value returned from the `computed()` will be the exact same object reference `{ f: 'f' }` both times. 

But this actually logs twice. This is different from `createMemo` from SolidJS or `createSelector` from NgRx, and I predict a lot of people will be surprised by this behavior when they encounter it. 

The reason for this behavior is that the Angular team doesn't want to have downstream code miss updates in case the object or array is mutated without getting a new reference. There is a `mutate` method on signals, after all. 

But there's a way to opt-in to more efficient behavior: Pass in `equal: (a, b) => a === b`. 

This is the only behavior I want. So I wrapped `computed` in my own function so I don't have to define that `equal` function everywhere:

```typescript
import { computed } from '@angular/core';

export function memo<T>(fn: () => T) {
  return computed(fn, { equal: (a, b) => a === b });
}
```

I suggest exporting this from an Nx lib, or giving the file a TypeScript [path alias](https://stackoverflow.com/questions/43281741/how-to-use-paths-in-tsconfig-json).

[StackBlitz](https://stackblitz.com/edit/stackblitz-starters-qbjsvs?file=src%2Fmain.ts)

## 2. Reuse logic, not state

The old implementation managed form state in NgRx, which is a perfectly reasonable thing to do, as I explained in [this article](https://dev.to/this-is-angular/why-and-how-to-manage-state-for-angular-reactive-forms-25fn).

However, unfortunately, most NgRx developers have never read [my article](https://dev.to/this-is-angular/progressive-reactivity-with-ngrxstore-and-ngxs-5bib#level-4-reusable-state-patterns) on how to avoid shooting yourself in the foot by coupling state logic with actual state. But there are a lot of reusable forms patterns, so the previous implementation centralized all form state in a single reducer. This is inconvenient in a few ways, but no page contains multiple forms, so it's not a disaster.

Still, I wanted to colocate forms with relevant state. I thought of 2 ways of doing this: Reusable functions, or a base class. Generally I like avoiding inheritance, but I found defining a base class the simplest strategy in this case. 

## 3. Signal update timing can break e2e tests

How many times do you think this will log?

```typescript
export class CountComponent {
  state = signal(5);
  constructor() {
    effect(() => console.log(this.state()));
    this.state.set(6);
    this.state.set(7);
  }
}
```

Answer: Twice! `5` and `7` will be logged. 

So if you have some code that results in a state change in an e2e test and expect the result to immediately be reflected, your test will break. In this app, the form state was not updated immediately, so I had to add a short wait before submitting the form. (One field was optional, so I couldn't simply wait for the submit button to become enabled.)

## 4. The auto-signal pattern is awesome

After rewriting 1856 lines of NgRx code to 1323 lines using the [auto-signal pattern](https://dev.to/mfp22/introducing-the-auto-signal-pattern-1a5h), I like it very much. It is very convenient to able to share signals in a way that doesn't block subscription information in RxJS streams. Both NgRx and `toSignal` block RxJS subscriptions, so from the existing NgRx implementation I was able to remove all of the manual initialization, resetting and refetching code. 

Look at all the code that became 100% unnecessary when RxJS was free to handle data dependencies:

```typescript
// apps/conduit/src/app/app.component.ts
  constructor(
    private readonly store: Store,
	...
  ) {}

  ngOnInit() {
    ...
        take(1),
        ...
      .subscribe(() =>
        this.store.dispatch(authActions.getUser()),
      );
  }

// libs/articles/data-access/src/lib/+state/article-list/article-list.effects.ts
export const loadArticle$ = createEffect(
  (
    actions$ = inject(Actions),
    ...
  ) => {
    return actions$.pipe(
      ofType(articleActions.loadArticle),
      ...
    );
  },
  { functional: true },
);
...
export const loadComments$ = createEffect(
  (
    actions$ = inject(Actions),
    ...
  ) => {
    return actions$.pipe(
      ofType(articleActions.loadComments),
      ...
    );
  },
  { functional: true },
);

// libs/articles/data-access/src/lib/+state/article-list/article-list.effects.ts
export const setListPage$ = createEffect(
  (actions$ = inject(Actions)) => {
    return actions$.pipe(
      ofType(articleListActions.setListPage),
      map(() => articleListActions.loadArticles()),
    );
  },
  { functional: true },
);

export const setListTag$ = createEffect(
  (actions$ = inject(Actions)) => {
    return actions$.pipe(
      ofType(articleListActions.setListConfig),
      map(() => articleListActions.loadArticles()),
    );
  },
  { functional: true },
);

export const loadArticles$ = createEffect(
  (
    actions$ = inject(Actions),
    store = inject(Store),
    ...
  ) => {
    return actions$.pipe(
      ofType(articleListActions.loadArticles),
      ...
    );
  },
  { functional: true },
);

// libs/articles/feature-article/src/lib/article-guard.service.ts
    this.store.dispatch(articleActions.loadArticle({ slug }));
    ...
      tap(() =>
        this.store.dispatch(
          articleActions.loadComments({ slug }),
        ),
      ),

// libs/articles/feature-article/src/lib/article.component.ts
  ngOnDestroy() {
    this.store.dispatch(articleActions.initializeArticle());
  }

// libs/articles/feature-article-edit/src/lib/article-edit.component.ts
  ngOnDestroy() {
    this.store.dispatch(formsActions.initializeForm());
  }

// libs/articles/feature-article-edit/src/lib/article-edit.routes.ts
        resolve: { articleEditResolver },

// libs/articles/feature-article-edit/src/lib/resolvers/article-edit-resolver.ts
export const articleEditResolver: ResolveFn<boolean> = (
  route: ActivatedRouteSnapshot,
) => {
  const slug = route.params['slug'];
  const store = inject(Store);

  if (slug) {
    store.dispatch(articleActions.loadArticle({ slug }));
  }

  return of(true);
};

// libs/auth/data-access/src/lib/+state/auth.effects.ts
export const getUser$ = createEffect(
  (
    actions$ = inject(Actions),
    ...
  ) => {
    return actions$.pipe(
      ofType(authActions.getUser),
      ...
    );
  },
  { functional: true },
);

// libs/auth/feature-auth/src/lib/login/login.component.ts
  ngOnDestroy() {
    this.store.dispatch(formsActions.initializeForm());
  }

// libs/auth/feature-auth/src/lib/register/register.component.ts
  ngOnDestroy() {
    this.store.dispatch(formsActions.initializeForm());
  }

// libs/home/src/lib/home.component.ts
  ngOnInit() {
        ...
        this.getArticles();
        ...
  }

// libs/home/src/lib/home.store.ts
  ngrxOnStateInit() {
    this.getTags();
  }

// libs/profile/data-access/src/lib/+state/profile.effects.ts
export const getProfile$ = createEffect(
  (
    actions$ = inject(Actions),
    ...
  ) => {
    return actions$.pipe(
      ofType(profileActions.loadProfile),
      groupBy((action) => action.id),
      ...
    );
  },
  { functional: true },
);

// libs/profile/data-access/src/lib/resolvers/profile-articles-resolver.ts

export const profileArticlesResolver: ResolveFn<boolean> = (
  route: ActivatedRouteSnapshot,
) => {
  const username = route.params['username'];
  const store = inject(Store);

  store.dispatch(
    articleListActions.setListConfig({
      config: {
        ...articleListInitialState.listConfig,
        filters: {
          ...articleListInitialState.listConfig.filters,
          author: username,
        },
      },
    }),
  );

  return of(true);
};

// libs/profile/data-access/src/lib/resolvers/profile-favorites-resolver.ts
export const profileFavoritesResolver: ResolveFn<boolean> = (
  route: ActivatedRouteSnapshot,
) => {
  const username = route?.parent?.params['username'];
  const store = inject(Store);

  store.dispatch(
    articleListActions.setListConfig({
      config: {
        ...articleListInitialState.listConfig,
        filters: {
          ...articleListInitialState.listConfig.filters,
          favorited: username,
        },
      },
    }),
  );

  return of(true);
};

// libs/profile/feature-profile/src/lib/profile.routes.ts
        resolve: { profileArticlesResolver },
...
        resolve: { profileFavoritesResolver },

// libs/profile/data-access/src/lib/resolvers/profile-resolver.ts
  store.dispatch(profileActions.loadProfile({ id: username }));

// libs/settings/feature-settings/src/lib/settings.component.ts
@UntilDestroy()
...
  ngOnInit() {
    this.store.dispatch(authActions.getUser());
    ...
    this.store
      .select(selectUser)
      .pipe(untilDestroyed(this))
      .subscribe((user) =>
        this.store.dispatch(
          formsActions.setData({ data: user }),
        ),
      );
  }

// libs/settings/feature-settings/src/lib/settings.store.ts
          map(() => this.store.dispatch(authActions.getUser())),
```

That is a lot of code.

It's a lot of potential bugs, too. For example: Why didn't `settings.component.ts` have an `ngOnDestroy` to reset the form state like the other form components did? Maybe that was a bug. Regardless, RxJS takes care of that stuff now.

**_Maintaining this logic is harder than learning [the auto-signal pattern.](https://dev.to/mfp22/introducing-the-auto-signal-pattern-1a5h)_**

## 5. The reactive variation of auto-signals is cleaner

There are 2 ways to implement state changes with auto-signals:
1. Method
2. Subject

### 1. Method

```typescript
export class CountStateService {
  // State
  state = signal(0);

  // Non-UI events and effects
  serverCount$ = inject(CountService).fetch();

  // Auto-signal connection
  connection$ = connectSource(this.state, this.serverCount$);

  // State change methods
  increment() {
    this.state.update(n => n + 1);
  }
}
```

This is most similar to what people are used to, but it encourages more of an imperative/non-unidirectional style of state management, so I prefer using subjects.

### 2. Subject

```typescript
export class CountStateService {
  // State
  state = signal(0);

  // All events/effects, including server and UI
  serverCount$ = inject(CountService).fetch();
  increment$ = new Subject<void>();

  // New state; similar to Redux/NgRx reducer
  newState$ = merge(
    this.serverCount$,
    this.increment$.pipe(map(() => this.state() + 1))
  )

  // Auto-signal connection
  connection$ = connectSource(this.state, this.newState$);
}
```

This has many of the same advantages as Redux/NgRx:

1. All logic that controls this state (other than the initial state) is centralized in the single declaration of `newState$`, which helps avoid bugs that arise from developers forgetting important context
2. Grouping relevant logic makes debugging easier
3. Event sources are not burdened with downstream concerns

Imagine we had multiple counts, and a button to reset all of them. With methods, we would need another method to call the other `increment` methods:

```typescript
export class CountComponent {
  count1StateService = injectAutoSignal(Count1StateService);
  count2StateService = injectAutoSignal(Count2StateService);

  resetAll() {
    this.count1StateService.reset();
    this.count2StateService.reset();
  }
}
```

Or we could export a subject:

```typescript
export const resetAll = new Subject<void>();
```

And import that into each state service, and react to its events inside the declaration `newState$` in each service. This is just like an NgRx action. Using it in the component is easy:

```typescript
export class CountComponent {
  count1StateService = injectAutoSignal(Count1StateService);
  count2StateService = injectAutoSignal(Count2StateService);
  resetAll$ = resetAll$;
}
```
```html
<button (click)="resetAll$.next()">Reset All</button>
```

Here's the difference in data flow between these approaches:

![the difference in data flow between method vs subject auto-signals](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z7vwxatm2erk00qfklp5.png)

The subject version might look more complicated, but it's actually less code, and that code is more appropriately colocated. 

**Don't follow the dogmatic tradition of creating an event handler for every event.** It is _entirely_ unnecessary. If you need flexibility, it can be downstream from the subject. The subject perfectly represents that something happened; it is a perfect abstraction, which means it acts as a simple, self-contained block that can be easily built upon by any future code. 

## 6. State changes and events should  be separate

Originally I wrote the form state base class like this:

```typescript
  newData$ = new Subject<any>();
  newDataChanges$ = this.newData$.pipe(
    map((data) => ({ ...this.state(), data })),
  );

  dataUpdate$ = new Subject<any>();
  dataUpdateChanges$ = this.dataUpdate$.pipe(
    map((data) => ({
      ...this.state(),
      data: { ...this.data(), ...data, touched: true },
    })),
  );

  reset$ = new Subject<void>();
  resetChanges$ = this.reset$.pipe(map(() => formsInitialState));

  newState$ = merge(
    this.newDataChanges$,
    this.dataUpdateChanges$,
    this.resetChanges$,
  );
```

But I came to see the separation between events and state as more primary than the separation between individual event/state change pairs. The events themselves don't have an opinion on how state should react, and may even be used in other services to cause unrelated state to react, so it felt better to group them like this:

```typescript
  newData$ = new Subject<any>();
  dataUpdate$ = new Subject<any>();
  reset$ = new Subject<void>();

  newState$ = merge(
    this.newData$.pipe(
      map((data) => ({ ...this.state(), data })),
    ),
    this.dataUpdate$.pipe(
      map((data) => ({
        ...this.state(),
        data: { ...this.data(), ...data, touched: true },
      })),
    ),
    this.reset$.pipe(map(() => initialFormState)),
  );
```

Once I made this change, I also noticed that `newState$` resembled an NgRx reducer:

```typescript
export const formReducer = createReducer(
  initialFormState,
  on(FormActions.newData, (data) => ({ ...this.state(), data })),
  on(FormActions.dataUpdate, (data) => ({
    ...this.state(),
    data: { ...this.data(), ...data, touched: true },
  })),
  on(FormActions.reset, () => initialFormState),
);
```

Basically, it felt more natural to me to group the state logic together. Also, when I have a hard time choosing between alternatives, I prefer the more succinct choice.

## 7. Base `connection$` observables should be `Observable<any>`

If you want to share state change logic by extending a base auto-signal class, TypeScript will infer the type of `connection$` and prevent you from changing it in a child class. We don't want this because it doesn't matter what kind of values `connection$` emits. So, we should just type it as `Observable<any>`:

```typescript
this.connection$ = merge(
  this.connection$,
  connectSource(this.newState$),
) as Observable<any>;
```

## 8. Some HTTP utilities are useful

HTTP observables often emit different values for success vs error. States need to react differently to these events, so I found myself writing some code over-and-over again, and eventually made these reusable utilities instead:

```typescript
export function filterSuccess<T>(
  source: RequestObservable<T>,
): Observable<T> {
  return source.pipe(
    filter(
      (result): result is T => !('errors' in (result as object)),
    ),
  );
}
export function filterError<T>(
  source: RequestObservable<T>,
): Observable<{ errors: {} }> {
  return source.pipe(
    filter(
      (result): result is { errors: {} } =>
        'errors' in (result as object),
    ),
  );
}
```

Here's an example of how I used these:

```typescript
  articles$ = toObservable(this.listConfig).pipe(
    debounceTime(100),
    switchMap((config) => this.articlesService.query(config)),
    share(),
  );
  articlesSuccess$ = this.articles$.pipe(filterSuccess, share());
  articlesFailure$ = this.articles$.pipe(filterError, share());

  ...

  newState$ = merge(
    ...
    this.articlesSuccess$.pipe(
      map((result): ArticleListState => {
        // return new state
      }),
    ),
    this.articlesFailure$.pipe(
      map((): ArticleListState => {
        // return new state
      }),
    ),
    ...
  );
```

## 9. `resetOnRefCountZero` can smooth transitions

Route guards present an awkward challenge for reactive apps. Imagine an auto-signal connected to an HTTP observable like this:

```typescript
export class NameStateService {
  state = signal('');

  nameFromServer$ = inject(NameService).fetch();

  connection$ = connectSource(this.state, this.nameFromServer$);
}
```

If we use `nameFromServer$` directly in a route resolver, the signal won't ever get the data. We should use `injectAutoSignal` like this:

```typescript
export function resolveName() {
  const state = injectAutoSignal(NameStateService).state;
  return toObservable(state).pipe(
    filter(s => s !== ''), // Wait for it to be defined
    take(1), // Resolvers need observables to complete
  );
}
```

But this observable completes _before_ the route is loaded and any subscribers within the route have the opportunity to inject this auto-signal. This means there is a very short period of time where `connection$` will have no subscribers. This triggers the `finalize` inside `connection$`, so state resets to `''`.

Thankfully, RxJS gives us a way to prevent `connection$` from completing for a period of time until new subscribers arrive. We can define `connection$` like this instead:

```typescript
  connection$ = connectSource(this.state, this.nameFromServer$).pipe(
    share({ resetOnRefCountZero: () => timer(1000) }),
  );
```

Our route will load much more quickly than `1000 ms`, but I thought it might be nice to wait that long in case someone accidentally clicks away from the route and comes back quickly. If they come back within a second, the old state will be preserved.

UX Note: Route guards should be avoided. It isn't a great user experience to click a button or link and wonder if it's broken or needs to be clicked again. I would much rather see some instant feedback as the route loads, even if that means looking at a spinner until the data arrives. I would even prefer getting kicked out of a route than watching a motionless app while it decides if I should be allowed to view a route. So I think route guards should be avoided in general. But I'm sure there are specific scenarios where they are necessary.

## 10. Accessing route data in services is a pain

Services don't belong to routes. Components do. Services belong to injectors. 

What if you inject a service inside a component? You would need to inject it into the route's root component, and it wouldn't be clear why it was there. It wouldn't be used in that specific component. 

In my case, it was a route resolver that first needed the service. Route resolvers are not part of the route itself, but they get route data passed in directly. This doesn't help if the service is trying to inject it. 

Previously, when I've had a service `providedIn 'root'` and wanted route data, I've just listened to router events and parsed the URL manually. It's usually not very hard, although I still hate it because it seems a little fragile. However, not even this option worked in this case, because when the resolver runs, the navigation events haven't been fired yet.

What I had to do was define a `BehaviorSubject` in the service for holding route data and imperatively set it from the resolver. Every time the route reloads, the resolver runs, so this will always be up-to-date. But having to write imperative code bothers me. When looking at the `BehaviorSubject` itself, there is no indication of where it gets its data from; you have to find all references to it and find where the resolver pushes data to it. But at least it worked.

Here's what it looked like:

```typescript
export const profileResolver: ResolveFn<boolean> = (
  route: ActivatedRouteSnapshot,
) => {
  const profileStateService = inject(ProfileStateService);

  profileStateService.routeParam$.next(route.params['username']);
...
```

## Bonus Tip: Type object literal returns

This tip doesn't just apply to signals or auto-signals, but when defining `newState$` with object literals I noticed some dangerously forgiving TypeScript behavior. It was allowing not just extra properties like usual (usually harmless), but incorrect types of expected properties. So, instead of relying on inference in this case, I suggest typing return values whenever defining `newState$` for nontrivial state, like this:

```typescript
export class SomeStateService {
  ...
  newState$ = merge(
    // Explicitly type return as State
    this.increment$.pipe(map((): State => ({
      ...this.state(),
      count: this.count() + 1,
    }))),
  )
}
```

This will give you much more useful TypeScript errors than just typing `newState$`, like `newState$: Observable<State> = ...`. Typing every single return type is repetitive, but the improved TS errors are worth it in my opinion.

## Conclusion

I am having a lot of fun with RxJS + signals. As an Angular community, we are learning a lot together. And remember that signals are still in developer preview, so experiment with them and let the Angular team hear your feedback. I hope they change `computed` to memoize everything, regardless of type. And personally, I wish it was named `memo`, but maybe that's just me. 

Thanks for reading!