loading...
Cover image for Keep your URL clean

Keep your URL clean

ayeletdn profile image Ayelet Dahan ・4 min read

In our app we can set the filters of a page through the URL. This is especially helpful when the user wants to share complex filters with peers.

Once the user has navigated into the page, we store the filter settings in session and we don't need the URL parameters any more. More over, if the user changes her filter settings, we update the session, but if she reloads the page with the URL, it would take precedence over the session settings and would give her a bad experience.

The solution is to remove the filters while storing them in the settings. But... We can't just remove all the url params because there may be others that do not belong to the filters. We need to remove each parameter at a time.

Let's get down to code.

ngOnInit() {
  this.route.queryParamMap.subscribe(paramMap => {
    this.filter(paramMap, 'f1');
    this.filter(paramMap, 'f2');

    // ...

  });
}

We subscribe to all the query params when our component initializes. Every time the queryMap changes when we're on this page, our filters will update. Don't forget to unsubscribe onDestroy!

private filter(paramMap:ParamMap, key:string) {
  if (!paramMap.has(key)) {return;}

  const value = paramMap.get(key);

  // Perform filtering here
  // ...

  // Set filter to session here
  // ..

  this.removeParamFromUrl(paramMap, key);
}

I like doing all the negative checks at the entrance to the function and exit if no work is to be done. It saves us some nice levels of indentation, at the cost of a slightly less readable negative-condition.

Once we're done with all the filtering and the session work, we can now safely remove the parameter from our URL.

private removeParamFromUrl(paramMap: ParamMap, key: string) {
  if (!paramMap.has(key)) { return; }

  const queryParams = {};
  paramMap.keys.filter(k => k != key).forEach(k => (queryParams[k] = paramMap.get(k)));
  this.router.navigate([], { queryParams, replaceUrl: true, relativeTo: this.route });
}

The if statement when we enter the function is just to make sure that we're not trying to remove a parameter that doesn't exist. It wouldn't have any visual effect if we didn't have this protection, but it saves us some cycles. Just to be sure, I looked up the source code for ParamMap and behind the scenes it uses a pure object, so ~O(1) to check. I digress.

We create a new query param dictionary and place in it only the keys we want. Finally, we navigate to the new route with one parameter less.

Hold On!! This is terrible!

  1. We loop over the param map ~O(n!).
  2. We are calling the router to navigate again and again.

Let's see if we can make this a little better? We'll work backwards now, starting from removeParamFromUrl:

private removeParamFromUrl(paramMap: ParamMap, keysToRemove: string[]) {
    const queryParams = {};
    const keysToKeep = paramMap.keys.filter(k => !keysToRemove.includes(k));
    keysToKeep.forEach(k => (queryParams[k] = paramMap.get(k)));

    this.router.navigate([], { queryParams, replaceUrl: true, relativeTo: this.route });
}

Now we're getting an array of keys that we want to get rid of, instead of a single key each time. We iterate over the keys of paramMap and keep only those that are not in included.

If we want to finish up quickly here, we can have our subscription call removeParamFromUrl with all our filters:

ngOnInit() {
  this.route.queryParamMap.subscribe(paramMap => {
    this.filter(paramMap, 'f1');
    this.filter(paramMap, 'f2');

    // ...
    this.removeParamFromUrl(paramMap, ['f1', 'f2' ...]);
  });
}

private filter(paramMap:ParamMap, key:string) {
  if (!paramMap.has(key)) {return;}

  const value = paramMap.get(key);

  // Perform filtering here
  // ...

  // Set filter to session here
  // ..

  // We got rid of the local call to removeParamFromUrl
}

And we'd be done.

But let's make it just a little cleaner? Again, at the cost of some readability.

private filter(paramMap:ParamMap, key:string):boolean {
  if (!paramMap.has(key)) {return false;}

  const value = paramMap.get(key);

  // Perform filtering here
  // ...

  // Set filter to session here
  // ..

  // We got rid of the local call to removeParamFromUrl
  // And returning true if the filter was applied.
  // Notice that we returned `false` in the `if` above!
  return true;
}

Now we'll handle the list filters in ngOnInit:

ngOnInit() {
  this.route.queryParamMap.subscribe(paramMap => {
    const filterKeys = ['f1', 'f2' ...];
    const keysToRemove = [];

    // filter and apply keys for removal
    filterKeys.forEach(k => {
      // filter and remove key only if filtered.
      this.filter(paramMap, k) && keysToRemove.push(k);
    });

    this.removeParamFromUrl(paramMap, keysToRemove);
  });
}

Here we iterate over the keys to remove. We make use the boolean condition - if the filter returned true the key gets added to keysToRemove. If the filter returned false, the second part of the condition will not evaluate at all and the key will not be added.

A final note about Readability: The question of readability is often personal and may depend on your own experience and convenience. But most importantly - think of the engineers who will come after you and will have to read and change your code. Will a junior developer be comfortable reading this? Will they notice and understand the condition?

Being a senior developer isn't only about writing the most efficient code. It's also about writing code that others can understand and change easily.

Posted on Jun 22 by:

ayeletdn profile

Ayelet Dahan

@ayeletdn

Developer and quality code advocate. Mainly in Javascript.

Discussion

markdown guide