angularangular-routingangular-resolver

Angular resolver not updating or refetching data, despite `runGuardsAndResolvers` set to 'always'?


I have a set of Angular routes, with a listing of entities, with two child routes for creation of such an entity and the editing of an existing entity. The listing of entities has a resolver attached to it to prefetch the data for the component before displaying it. These routes can be summarised as follows, see further down for how these routes are described in code.

However, if I am at /items/create, and successfully create an item, navigating "back" to /items or any edit route, or even back to / will not result in my resolver fetching updated data like I expect. This is despite having the runGuardsAndResolvers property set to "always". My understanding is that this property should enable to functionality I'm looking for.

Why is this, and how can I enable the functionality I'm looking for, without doing something like subscribing to router events in my component and duplicating logic.

Routes

const itemRoutes: Routes = [
    {
        path: '', // nb: this is a lazily loaded module that is itself a child of a parent, the _actual_ path for this is `/items`. 
        runGuardsAndResolvers: 'always',
        component: ItemsComponent,
        data: {
            breadcrumb: 'Items'
        },
        resolve: {
            items: ItemsResolver
        },
        children: [
            {
                path: 'create',
                component: CreateItemComponent,
                data: {
                    breadcrumb: 'Create Item'
                }
            },
            {
                path: ':itemId/edit',
                component: EditOrArchiveItemComponent,
                data: {
                    breadcrumb: 'Editing Item :item.name'
                },
                resolve: {
                    item: ItemResolver
                }
            }
        ]
    }
];

ItemsResolver

@Injectable()
export class ItemsResolver implements Resolve<ItemInIndex[]> {
    public constructor(public itemsHttpService: ItemsHttpService, private router: Router) {}

    public resolve(ars: ActivatedRouteSnapshot, rss: RouterStateSnapshot): Observable<ItemInIndex[]> {
        return this.itemsHttpService.index().take(1).map(items => {
            if (items) {
                return items;
            } else {
                this.router.navigate(['/']);
                return null;
            }
        });
    }
}

ItemsHttpService

(Posting at request)

@Injectable()
export class ItemsHttpService  {

    public constructor(private apollo: Apollo) {}

    public index(): Observable<ItemInIndex[]> {
        const itemsQuery = gql`
            query ItemsQuery {
                items {
                    id,
                    name
                }
            }
            `;

        return this.apollo.query<any>({
            query: itemsQuery
        }).pipe(
            map(result => result.data.items)
        );
    }
}

Solution

  • First of all.

    This is not how you should use a Resolver.

    You should use a Resolver only if it is critical necessary for your route that the needed data exist. So a Resolver makes sense when you have a route for a dedicated piece of data and you want to make sure this data exists before you continue with loading the component.

    Let's take your Item App as an Example and face the problem from the user point of view.

    /items

    The user navigates to /items and what he is gonna see first is nothing. That's because the Resolver is fetching all items before the user can reach the component. Only after the resolver fetched all items from the api, the user gets delegated to the ItemsComponent.

    But this is not necessary and bad user experience.

    Why not delegate the user directly to ItemsComponent. If you have for example an awesome table with lot of buttons and fancy cool css, you can give your app time to render those elements while your Service is fetching the data from the api. To indicate the user the data is loading you can show a nice progress-spinner(-bar) inside the table until the data arrives.

    /items/create

    Same here. The user has to wait until the Resolver fetched all the data from your api before he is able to reach the CreateComponent. It should be clear that this an overkill to fetch all items just only the user can create an item. So there is no need for an Resolver

    /items/:itemId/edit

    That's the best place for a Resolver. The user should be able to route to this component if and only if the item exists. But as you defined a Resolver to fetch all items when one of your child routes gets activated, the user faces the same bad user experience as with the previous routes.

    Solution

    Remove the ItemsResolver from your Routes.

    const routes: Routes = [
      {
          path: '',
          component: ItemsComponent,
          children: [
            {
              path: '',
              component: ItemsListComponent,
              data: {
                breadcrumb: 'Items'
              },
            },
            {
              path: 'create',
              component: CreateItemComponent,
              data: {
                breadcrumb: 'Create Item'
              },
            },
            {
              path: ':itemId/edit',
              component: EditOrArchiveItemComponent,
              data: {
                breadcrumb: 'Editing Item :item.name'
              },
              resolve: {
                item: ItemResolverService
              }
            }
          ]
      }
    ];

    You can define a BehaviorSubject in your ItemsService which holds your last fetched Items like a cache. Everytime a user is routing back from create or edit to ItemsComponent your app can immediately render the cached items and in the meanwhile your service can sync the items in the background.

    I implemented a small working example App: https://stackblitz.com/github/SplitterAlex/stackoverflow-48640721

    Conclusion

    A Resolver makes only sense at the edit items route.

    Use Resolver cautiously!