Retrieve route data with resolver function in Angular
Tags: angular, webdev, route
Published: 2023-11-05 08:55:50

##Introduction

In this blog post, I demonstrate the technique of using data resolver function to retrieve data during route change.  When route finishes activation, component has access to the route data and can render it in template or to manipulate it to derive new states.  

I was working on a fake store demo where the first page displays all the products in a list. When user clicks a product name, user is routed to the product details page. This use case is very straightforward but I actually spent some time to implement the final solution.  

The initial solution was to make a HTTP request to retrieve an Observable of product, and then use `NgIf` and `AsyncPipe` to resolve the Observable in the template.  Angular introduced `Signal` and I thought to store the product in a Signal and render the template with Signal value.  The solution was not elegant and I had to scratch it. Finally, I implemented a data resolver function to retrieve a product by id. When the product details page is routed completely, I extracted the product from the route data and used it within the component.   

###Use case of the demo

In the fake store demo, I call the product API to retrieve all the products and render them in ProductListComponent.  When user clicks the name of the product,  I call another API to retrieve the details by id and display the data on ProductDetailsComponent.   

```typescript
// routes.ts

export const routes: Routes = [
  {
    path: 'products',
    loadComponent: () => import('./products/product-list/product-list.component').then((m) => m.ProductListComponent),
    title: 'Product list',
  },
  {
    path: 'products/:id',
    loadComponent: () => import('./products/product-details/product-details.component').then((m) => m.ProductDetailsComponent),
    title: 'Product',
  },
];
```

This use case is typical of a CRUD application but the solution that obtains the product actually required 3 iterations to develop.

**Attempt 1:** Retrieving an Observable of product by id, and resolving the Observable in the HTML template using NgIf and AsyncPipe. 

**Attempt 2:** Applying toSignal to convert Observable to Signal and display the Signal value in the template.  This solution was actually over complicated and it made thing worse

**Attempt 3:** Using data resolver function to retrieve an Observable or product. With the help of withComponentInputBinding, the product is available in ProductDetailsComponent as an input. Then, I applied the input in the inline template to display the product values

In the next few sections, I am going to show how I iteratively implemented the resolve function to return the product and to use it in the component. 

###Solution 1: Getting an Observable from API and resolving it in the inline template

With the help of `withComponentInputBinding`, the path param (id) is an input of `ProductDetailsComponent`.  I used the id to retrieve the product, and resolved the Observable in the inline template using NgIf and AsyncPipe.

```typescript
// main.ts

bootstrapApplication(App, {
  providers: [provideHttpClient(), provideRouter(routes, withComponentInputBinding())]
});
```

```typescript
// product.service.ts

const PRODUCTS_URL = 'https://fakestoreapi.com/products';

@Injectable({
  providedIn: 'root'
})
export class ProductService {
  private readonly httpClient = inject(HttpClient);

  getProduct(id: number): Observable<Product | undefined> {
    return this.httpClient.get<Product>(`${PRODUCTS_URL}/${id}`)
      .pipe(catchError((err) => of(undefined)));
  }
}
```

```typescript
// product-details.component.ts

@Component({
  selector: 'app-product-details',
  standalone: true,
  imports: [AsyncPipe, NgIf],
  template: `
    <div>
      <div class="product" *ngIf="product$ | async as product">
        <div class="row">
          <img [src]="product?.image" [attr.alt]="product?.title" width="200" height="200" />
        </div>
        <div class="row">
          <span>id:</span><span>{{ id }}</span>
        </div>
        <div class="row">
          <span>Category: </span><span>{{ (product?.category '' }}</span>
        </div>
        <div class="row">
          <span>Description: </span><span>{{ product?.description || '' }}</span>
        </div>
        <div class="row">
          <span>Price: </span><span>{{ product?.price || '' }}</span>
        </div> 
      </div>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ProductDetailsComponent implements OnInit {

    @Input({ required: true, transform: numberAttribute })
    id!: number;

    productService = inject(ProductService);
    product$!: Observable<Product | undefined>;

    ngOnInit() {
          this.product$ = this.productService.getProduct(this.id);
    }
}
```

However, I preferred not to deal with `Observable`, `NgIf` and `AsyncPipe` in the component and the HTML template.   If I don't use Observable, I will use Signal that tracks reactivity in an application. 

Let me refactor the solution to use Signal and display the Signal value in the inline template.    

###Solution 2: Converting Observable to Signal and display Signal value in the inline template

`toSignal()` is a function that converts `Observable` to `Signal` and I thought it is the only thing I needed in `ngOnInit`. However, the compiler issued an error because `toSignal()` was not called in an injection context. To fix this error,  I performed the logic in the callback function of `runInInjectionContext`.  

```typescript
// product-details.component.ts

injector = inject(Injector);
product: Signal<Product | undefined> = signal(undefined); 

ngOnInit() {
    runInInjectionContext(this.injector, () => {
      this.product = toSignal(this.productService.getProduct(this.id),
            { initialValue: undefined });
    });
}
```

With Signal, I don't have to import `NgIf` and `AsyncPipe`, and every occurrence of `product` changes to `product()`.

```html
<div>
      <div class="product">
        <div class="row">
          <img [src]="product()?.image" [attr.alt]="product()?.title" width="200" height="200" />
        </div>
        <div class="row">
          <span>id:</span><span>{{ id }}</span>
        </div>
        <div class="row">
          <span>Category: </span><span>{{ product()?.category || '' }}</span>
        </div>
        <div class="row">
          <span>Description: </span><span>{{ product()?.description || '' }}</span>
        </div>
        <div class="row">
          <span>Price: </span><span>{{ product()?.price || '' }}</span>
        </div> 
      </div>
</div>
```

When comparing between the Observable and the Signal solutions, the latter one added unnecessary complexities such as `inject(Injector)` and  `runInInjectionContext(...)`.  I would rather revert to the original solution than to use `toSignal` for the sakes of using `Signal`. 

Then, I formulated another solution that is using data resolver function to obtain the product during route change.  Similarly, `withComponentInputBinding` should give me a product input in ProductDetailsComponent.

###Solution 3: Using data resolver function to return the product to ProductDetailsComponent

The data resolver function accepts a route and returns either an Observable or a Promise. Therefore, I extracted the id from the URL and passed it to the Product Service to retrieve the Observable of product.

```typescript
// product.resolver.ts

export const productResolver = (route: ActivatedRouteSnapshot) => {
  const productId = route.paramMap.get('id');

  if (!productId) {
    return of(undefined);
  }

  return inject(ProductService).getProduct(+productId);
}
```

In `route.ts`,  I modified the routes configuration to assign productResolver to the resolve property of `products/:id` path.

```typescript
// route.ts

export const routes: Routes = [
  {
    path: 'products/:id',
    loadComponent: () => import('./products/product-details/product-details.component').then((m) => m.ProductDetailsComponent),
    title: 'Product',
    resolve: {
      product: productResolver,
    }
  },
];
```

Next, I could clean up `ProductDetailsComponents` because the product resolver eliminated the previous logic in `ngOnInit`.

```typescript
// product-details.component

@Component({
  selector: 'app-product-details',
  standalone: true,
  template: `
    <div>
      <div class="product">
       <div class="row">
          <img [src]="product?.image" [attr.alt]="product?.title || 'product image'"
            width="200" height="200"
           />
        </div>
        <div class="row">
          <span>id:</span>
          <span>{{ product?.id || '' }}</span>
        </div>
        <div class="row">
          <span>Category: </span>
          <span>{{ product?.category || '' }}</span>
        </div>
        <div class="row">
          <span>Description: </span>
          <span>{{ product?.description || '' }}</span>
        </div>
        <div class="row">
          <span>Price: </span>
          <span>{{ product?.price || '' }}</span>
        </div> 
      </div>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ProductDetailsComponent {
  @Input()
  product: Product | undefined = undefined; 
}
```

Finally, I replaced all occurrences of `product()` with `product` in the inline template. 

The following Stackblitz repo shows the final results:

{% embed https://stackblitz.com/edit/stackblitz-starters-pp9rg9?file=src%2Fproducts%2Fresolvers%2Fproduct.resolver.ts %}

This is the end of the blog post and I hope you like the content and continue to follow my learning experience in Angular and other technologies.

##Resources:
- Github Repo: https://github.com/railsstudent/ng-route-resolve-function-demo
- Stackblitz: https://stackblitz.com/edit/stackblitz-starters-pp9rg9?file=src%2Fproducts%2Fresolvers%2Fproduct.resolver.ts
- Angular documentation:  https://angular.io/api/router/ResolveFn