URL and routing

Photo by Ed Vázquez on Unsplash

URL and routing

Routing on the modern web

Most web pages and applications deal with URLs in some way. This could be an action like crafting a link with certain query parameters or URL-based routing in a single-page application (SPA).

A URL is just a string that complies with some syntax rules as defined in RFC 3986, “Uniform Resource Identifier (URI): Generic Syntax”. There are several component parts of a URL that you may need to parse or manipulate. Doing so with techniques like regular expressions or string concatenation isn’t always reliable.

Today, browsers support the URL API. This API provides a URL constructor that can create, derive, and manipulate URLs. This API was somewhat limited at first, but later updates added utilities like the URLSearchParams interface that simplifies building and reading query strings.

Browser Location and Navigation

The location property of the Window object refers to a Location object, which represents the current URL of the document displayed in the window and also defines methods for making the window load a new document.

A window location is an object that contains a lot of information about the URL

Overview

Parsing URLs

// URL : http://example.com/page?name=John&age=25&city=New%20York
// location.search = ?name=John&age=25&city=New%20York
function urlArgs(url) {
   url = new URL(url);
   const result = [];
   url.searchParams.forEach((value,key)=>result.push({key,value}));
   return result;
}

[{key: 'name', value: 'John'}
{key: 'age', value: '25'}
{key: 'city', value: 'New York'}]
💡
Techniques like regular expressions or string concatenation isn't always reliable, especially nowadays when browsers support the URL API and URLSearchParam interface, that simplifies building and reading the query string

Methods of window.location

  • window.location.assign(url): Loads a new document at the specified URL

  • window.location.replace(url): Replaces the current document with a new one at the specified URL without adding a try to the browser's history

  • window.location.reload(forceReload): Reloads the current document.If the forceReload parameter is set to true

Navigate to a new page

//Using window.location.assign
window.location.assign('google.com');

//Using window.location.href
window.location.href="google.com";

//Prevent users from clicking the back button to return to previous page
window.location.replace('google.com');

New URL API

Resolving a Relative URL

💡
If the first argument starts with a leading slash, the pathname of the base URL is ignored, and the new URL is relative to the root of the base URL:
function resolveUrl(relativePath, baseUrl) {
  return new URL(relativePath, baseUrl).href;
}

// https://example.com/api/users
console.log(resolveUrl('/api/users', 'https://example.com'))
// https://example.com/api/v1/users
console.log(resolveUrl('/api/v1/users', 'https://example.com'));
// https://example.com/api/v1/users
console.log(resolveUrl('/api/v1/users', 'https://example.com/api/v2'));
// https://example.com/api/v1/users
console.log(resolveUrl('../v1/users/', 'https://example.com/api/v2'));
// https://example.com/api/v1/users
console.log(resolveUrl('users', 'https://example.com/api/v1/groups'));
💡
React router v6 works very similar to the new URL APIs

Better reading and writing URL

const url = new URL("https:google.com");
url.searchParams.set('model',"model");
url.searchParams.set('locale',"locale");
url.searchParams.set('text',"text");

url.toString();

//Instead of
const url = 
`https:google.com?model=${model}&locale=${locale}&text=${text}`
// Bad -> this is too verbose and the value is not even encode it

Encoding reserved characters in a query parameters

💡
THIS IS SIMILAR TO REGULAR EXPREESION, WHERE WE ALSO HAVE SOME RESERVED CHARACTER LIKE \w \d etc
const url = new URL('https://example.com/api/search');

//Contrived example string demonstrating several reserved characters
url.searchParams.append('q', 'admin&user?luke');

//Result
https://example.com/api/search?q=admin%26user%3Fluke
💡
The URL contains %26 in place of &, and %3F in place of ?. These characters have special meaning in a URL. ? indicates the beginning of the query string and & is a separator between parameters.

Reading query parameters

function getQueryParameters(inputUrl) {
  // Can't use an object here because there may be multiple
  // parameters with the same key, and we want to return all parameers.
  const result = [];

  const url = new URL(inputUrl);

  // Add each key/value pair to the result array
  url.searchParams.forEach((value, key) => {
    result.push({ key, value });
  });

  // Results are ready!
  return result;
}

💡
URLSearchParams is not an array but it implements all array methods to mimic behaviors of an array When you loop through the forEach function, it will give you key and name of query parameter

Creating a simple client-side router

How does react-router work?

History.pushState and popState events

The global history object's pushState method changes the current URL without reloading the page. It adds the new URL to browser's history

  • First, an object containing arbitrary data to associate with the new history entry. This state data is available from the popstate event as well.

  • The second argument is unused, but must be given. You can use an empty string here.

  • Finally, the new URL. This can be an absolute URL, or a relative path. If you use an absolute URL, it must be on the same origin as the current page or the browser throws an exception.

// Route definitions. Each route has a path and some content to render.
const routes = [
  { path: '/', content: '<h1>Home</h1>' },
  { path: '/about', content: '<h1>About</h1>' }
];

function navigate(path, pushState = true) {
  // Find the matching route and render its content
  const route = this.routes.find(route => route.path === path);

  // Be careful using innerHTML in a real app!
  document.querySelector('#main').innerHTML = route.content;

  if (pushState) {
    // Change the URL to match the new route
    history.pushState({}, '', path);
  }
}

With this navigate function, we can override the behavior of link

<a href="/">Home</a>
<a href="/about">About</a>
document.querySelectorAll('a').forEach(link => {
  link.addEventListener('click', event => {
    // Prevent the browser from trying to load the new URL from the server!
    event.preventDefault();
    navigate(link.getAttribute('href'));
  });
});

To make this a full solution, there is one more necessary piece. If you click one of these client-side routes, then click the browser’s Back button, nothing happens. This is because the page isn’t actually navigating but just popping the previous state from the router. To handle this scenario, you also need to listen for the browser'spopstate event and render the correct content.

window.addEventListener('popstate', () => {
  navigate(window.location.pathname, false);
});

When the user clicks the Back button, the browser fires the popstate event. This changes the page URL back, and you just need to look up the content for the route matching the current URL. In this case, you don’t want to call pushState because that adds a new historical state. This probably isn’t what you want since you just popped an old history state off the stack.