Using the url for state with Angular
Tags: angular, javascript, webdev, tutorial
Published: 2020-02-29 22:19:55

A pretty common use-case for storing values in the `url` in the form of **query strings** are filters.
Storing filters in the `url` ensures that everyone with the link will see the same filterable content in their browsers.

This is really great for ecommerce where people will often discuss with friends or ask for help before purchasing a product, and in doing so, might send a link of their filtered products along to their friends.

While it may seem like a small thing, it´s actually a really great user experience to store product filters in the `url`.

> If users refresh their browser by hitting **F5**, they will also return back to the same screen they were on before. Neat!

**But what about single page applications? 🤔**

Typically, in a SPA, routing happens on the client. Meaning our frameworks or libraries hook into the browser history, altering the state and updating the `url` without a full page refresh.
There is no roundtrip to the server and the whole page does not need to be rendered all over, which in itself, is **also really nice**!

However, since the `url` in this case is really only responsible of getting the application resources to the user upfront, it is not used for fetching any related data for the route.

In Angular we normally use `services` to fetch data. These services are then imported into our `components` which use these `services` to fetch data and render the view:

```typescript
@Component({
  selector: 'product-list',
  template: `
  <ul>
      <li *ngFor="let product of products">{{ product.name }} {{ product.genre }} {{ product.platform }}</li>
  </ul>
  `
})
export class ProductListComponent implements OnInit {
  products: Product[] = [];

  constructor(private service: ProductService) {}

  ngOnInit() {
      this.service.getProducts().subscribe(_products => {
          this.products = _products;
      })
  }
}
```
The example above is pretty simple. We inject the `ProductService` and during the `ngOnInit` lifecycle of the component, we subscribe to `getProducts()` which returns
a list of products when the api call is done. The response, a list of products in our case, is then set to the class field `products`.

This field is then used in the `template` to render the list of products in a normal unordered list.

**Adding filters** 💡

Now lets say that we want to add two types of filters to our product list, genre and platform, to narrow down the list of games.

Instead of storing the filter options on the component or in a service, let us use the `url` instead and store our filters as query params.

To access the query params we use the `ActivatedRoute` service within our component.
And to update the query params, we simply navigate 🤭 -- yeah, it is that simple!

Getting the query params look something like:

```typescript
constructor(private route: ActivatedRoute) {}

ngOnInit() {
    this.route.queryParams.subscribe(queryParams => {
        /**
         * If the query-string is '?genre=rpg&platform=xbox'
         * the queryParams object will look like
         * { platform: 'xbox', genre: 'rpg }
         * */
    });
}
```

Depending on where you´re navigating from, setting query parameters can be done in a few different ways:

```html
<button [routerLink]="[]" [queryParams]="{ genre: 'rpg' }" queryParamsHandling="merge">RPG</button>
```

Here we use the `routerLink` directive on a `button` to navigate to the same route as we´re on using `[routerLink]="[]"` and set the query parameter `genre` to `rpg`, using the `queryParams` property.

We use the `queryParamsHandling="merge"` here to make sure we merge our queryParams with the already existing queryParams, overriding ones with identical keys. 

So clicking another `routerLink` that has `[queryParams]="{ genre: 'racing' }"` will directly override our current `genre` query param, but a `routerLink` with `[queryParams]="{ platform: 'xbox' }"` would merge the two into `[queryParams]="{ genre: 'rpg', platform: 'xbox' }"`.

Another option is doing it from within a component method using the `router` service:

```typescript
constructor(private router: Router) {}

updateQueryParameters() {
    this.router.navigate(
        [], 
        { 
            queryParams: { 
                genre: 'rpg'
            }, 
            queryParamsHandling: 'merge' 
        }
    );
}
```

Using the `router.navigate` function we navigate to the current route `[]` and set the `queryParams` object and the `queryParamsHandling` option, just like we did with the directive.

For the purpose of this example i´ll be using simple links to update the filters like so:

```html
<div class="product-filters">
    <p>genres: </p>
    <a [routerLink]="[]" [queryParams]="{ genre: 'rpg' }" queryParamsHandling="merge">rpg</a>
    <a [routerLink]="[]" [queryParams]="{ genre: 'platformer' }" queryParamsHandling="merge">platformer</a>
    <a [routerLink]="[]" [queryParams]="{ genre: 'racing' }" queryParamsHandling="merge">racing</a>
    <a [routerLink]="[]" [queryParams]="{ genre: null }" queryParamsHandling="merge">clear</a>
</div>

<div class="product-filters">
    <p>platforms: </p>
    <a [routerLink]="[]" [queryParams]="{ platform: 'playstation' }" queryParamsHandling="merge">playstation</a>
    <a [routerLink]="[]" [queryParams]="{ platform: 'switch' }" queryParamsHandling="merge">switch</a>
    <a [routerLink]="[]" [queryParams]="{ platform: 'xbox' }" queryParamsHandling="merge">xbox</a>
    <a [routerLink]="[]" [queryParams]="{ platform: null }" queryParamsHandling="merge">clear</a>
</div>
```
<small>*Above i´ve added an extra button to each list of filters to clear that particular filter from the url, by simply setting `null`*</small>

Now, clicking on the link for **platformer** under genres, followed by the link to **xbox** under platforms we end up with a url like this:

```
localhost:4200/?genre=platformer&platform=xbox
```

Great, now we can set our filters in the `url` and have our component listen to these changes using the `ActivatedRoute` service. So far so good!

**Putting it all together**

Let us combine our `productService` with our `queryParams` observable stream from the `ActivatedRoute` service to make sure the service is called each time our filters are updated.

```typescript
@Component({
  selector: 'product-list',
  templateUrl: './product-list.component.html'
})
export class ProductListComponent {

  products$: Observable<Product[]>;

  constructor(private route: ActivatedRoute, private productService: ProductsService) {
    this.products$ = this.route.queryParams.pipe(switchMap(params => {
      const filters = {
        platform: params.platform || "",
        genre: params.genre || ""
      };

      return this.productService.getProducts(filters);
    }));
  }
}
```

We´ve changed our class field `products: Product[]` to `products$: Observable<Product[]>` as it will now be an observable stream of data that gets updated when our filters update.

We then listen to changes on the `queryParams` stream from the `ActivatedRoute` service and and use `switchMap` to switch to the `productService.getProducts` observable, passing in the filters we get from the `queryParams`.

> Since our queryParams could be non-existent we create an object and set default values to `""` for both `genre` and `platform` to make sure we provide a sensible default.

The updated template for the `ProductListComponent` now looks like:

```html
<ul>
    <li *ngFor="let product of products$ | async;">{{ product.name }} {{ product.genre }} {{ product.platform }}</li>
</ul>
```

Now, everytime we update a query parameter using one or more of our filters, the list of products will automatically get updated with new data from the service.

**That is pretty much all there is to it!** 🎉

An added benefit of using the `url` for state such as this, is that the component that updates the filters does not need to be directly tied to the component showing the product list. 
They could live in completely separate parts of our application. 

For those using a state management library such as [ngrx](https://ngrx.io), this might seem familiar.
Updating the query parameters here is much like dispatching an `action`, and listening to changes in the queryParams state of the `url` using the `router`, is like listening to store changes with `selectors` in ngrx.

**Show me the code** 💻

I have created an example repository for the `ProductListComponent` example used in this article, you can find it here: [query-params-filter](https://github.com/Phhansen/query-params-filter)

I hope this has been useful to you in one way or another! 🙌

*Also posted on my [personal blog](https://www.hjorthhansen.dev/url-as-state-in-angular/)*