angularangular-dependency-injection

Combine route resolver provider and component provider in Angular


I want to 'attach' a signal store to one of my components. I intend to provide this at component level and not at the root, as the state is specific to that component.

However I like to use route resolvers to prevent navigation to broken pages - in this case I'd not load my component if there was a problem retrieving the data.

Without providing my signal store at root, are there any patterns that can help me share the data between resolver, component and signal store, such that I can get the benefits of both the resolver and the signal store?


Solution

  • You can use the route providers array. In doing so, the service will be instantiated at the route level.

    It'd look like this:

    bootstrapApplication(App, {
      providers: [
        provideRouter([
          {
            path: 'a',
            component: AComponent,
            providers: [Store],
            resolve: {
              data: (
                route: ActivatedRouteSnapshot,
                state: RouterStateSnapshot
              ) => ({
                storeInstanceNumber: inject(Store).instanceNumber,
              }),
            },
          },
          {
            path: 'b',
            component: BComponent,
            providers: [Store],
            resolve: {
              data: (
                route: ActivatedRouteSnapshot,
                state: RouterStateSnapshot
              ) => ({
                storeInstanceNumber: inject(Store).instanceNumber,
              }),
            },
          },
        ]),
      ],
    });
    

    And to show you one component, it'd look like this:

    @Component({
      selector: 'app-a',
      standalone: true,
      template: `
        <h1>Component A</h1>
        <div>Store instance number: {{ store.instanceNumber }}</div><br />
        <div>Route data: <pre>{{ routeData | async | json }}</pre></div>
      `,
      imports: [CommonModule],
    })
    export class AComponent {
      public readonly store = inject(Store);
      public readonly routeData = inject(ActivatedRoute).data;
    }
    

    This is just to prove that you can inject the store in both the resolver and the component.

    Here's a live example on Stackblitz.

    Do note that in doing so, the Store provided at the route level is only created once per route, on the contrary to once per component instantiation. Depending on what you want, this could either be a feature or an issue: Leaving a route and coming back to it, you'll still have all the data available.

    No matter what you want, it's achievable in both cases. Here's an example that'll give a new instance every time we go back to the route a, and keep the same Store instance for the route b:

    bootstrapApplication(App, {
      providers: [
        provideRouter([
          {
            path: 'a',
            component: AComponent,
            resolve: {
              store: (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) =>
                // small hack to create a new Store instance every time we open that route
                Injector.create({
                  providers: [Store],
                  parent: inject(Injector),
                }).get(Store),
            },
          },
          {
            path: 'b',
            component: BComponent,
            // here we'll get the same Store instance if we leave and come back
            providers: [Store],
            resolve: {
              data: (
                route: ActivatedRouteSnapshot,
                state: RouterStateSnapshot
              ) => ({
                storeInstanceNumber: inject(Store).instanceNumber,
              }),
            },
          },
        ]),
      ],
    });
    

    For component B, you don't need to change anything and you can inject the Store as usual.

    For component A, we need to slightly tweak the code as we now receive the Store instance in the route data:

    // this component receives a new Store instance every time it's been created
    @Component({
      selector: 'app-a',
      standalone: true,
      template: `
        <h1>Component A</h1>
        <div>Store instance number: {{ store.instanceNumber }}</div><br />
      `,
      imports: [CommonModule],
    })
    export class AComponent {
      public readonly store:Store = inject(ActivatedRoute).snapshot.data['store']
    }
    

    Here's another Stackblitz for this solution. If you alternate and go to a, then b a few times, you'll see in the view that the counter on a increases but it remains to the same value in b.