Hooking up signals

Recently in our study group the following question came up:

How do I get the id from my route params into my service, so that I can use that in a httpResource?

Getting the id into the component was the easy part (and already done): Just use withComponentInputBinding() on your router configuration and create a signal input with the same name as your route parameter.

// the route
const routes: Routes = [
  {
    path: 'club/:clubId',
    component: Club,
    // ...
  },
];

// the service
@Injectable({...})
export class ClubService {
  getMatches(): Resource<Club> {
    // we'll come to this later...
    return httpResource(...);
  }
}

// the component
@Component({...})
export class Club {
  readonly clubService = inject(ClubService);
  readonly clubId = input<string | undefined>();
  // ...
}

The first thought that may come to mind is something like “I have my id in the component, so I have to read that signal and push its value into the service, so that the service can use that in the resource to fetch the data”. And it may be implemented in such a way (don’t look too close, it’s just scribbled and not compiled/tested):

// the service
@Injectable({...})
export class ClubService {
  readonly clubId = signal<string>();
  readonly matchParams = computed(() => {
    const clubId = this.clubId();
    return clubId == null
      ? undefined
      : { clubId };
  });

  getMatches(): Resource<Club> {
    // we'll come to this later...
    return httpResource(() => ({
      url: '/api/club',
      params: this.matchParams(),
    }));
  }
}

// the component
@Component({...})
export class Club implements OnInit {
  // same as above

  readonly matches = this.clubService.getMatches();

  ngOnInit() {
    // we cannot read the input in the constructor
    // because at that time it's not set yet
    this.clubService.clubId.set(this.clubId());
  }
}

The main problem with this kind of code is, that the input signal is only read once when initializing the component. If it changes in the lifetime of the component, it will not be pushed to the service to update the resource.

The next problem is, that the clubId signal in the service may be used by multiple components and whenever a component updates the signal, every other will receive new data. That may be something you intend to do, but if not, you run into problems.

The first problem could be solved with an effect, so whenever the input updates, it will be synced with the service.

ngOnInit() {
  effect(() => {
    this.clubService.clubId.set(this.clubId());
  });
}

But we all know:

“Don’t use effects (if you don’t have to)!”

Ben Lesh / Alex Rickabaugh

So what should we do instead?

In many tutorials about signals we are taught how to create or read them inside a computed, to build reactive chains. But there are not that much that provides us with some pattern, if the computed part of a signals lives in another file (aka service etc.).

And this is the main lesson from this debugging session:

Whenever you read from a signal to pass it into another signal, you may be doing something wrong. It’s like doing a “subscribe” inside a “subscribe” of an observable. We all learned that that’s bad practice. Signals should always be connected through some reactive graph. You “just” have to find the right way to “hook them up”. And that could mean, that you don’t want to read the signal’s value, but pass the whole signal to the service, so that the service can use it to build its reactive graph and everthing “just” works.

// the service
@Injectable({...})
export class ClubService {
  getMatches(clubId: Signal<string | undefined>): Resource<Club> {
    const matchParams = computed(() => {
      const clubId = this.clubId();
      return clubId == null
        ? undefined
        : { clubId };
    });
    return httpResource(() => ({
      url: '/api/club',
      params: matchParams(),
    }));
  }
}

// the component
@Component({...})
export class Club implements OnInit {
  readonly clubService = inject(ClubService);
  readonly clubId = input<string | undefined>();

  readonly matches = this.clubService.getMatches(this.clubId);
}

No more “effect” or “ngOnInit” – just signals and everything is reactive… 😎

What patterns do you use? Tell or show us in our study groups, we are very interested in learning new things together! #WeLearnTogether


Comments

Leave a Reply