Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?

Everything I Know About UI Routing

Ingredients

  1. Location
    1. pathname
    2. state
    3. search/query
    4. hash
  2. Path
    1. Dynamic Segment
    2. Wild Card
    3. Regex
  3. Matching
    1. URI
    2. Parameters
    3. State
    4. Query
  4. History
    1. Listener
    2. State
    3. History Stack (push/replace/index)
  5. Routes
    1. Path
    2. Data
    3. Validation
  6. Links
    1. href
    2. state
    3. active status
  7. File System API

tl;dr On Client Side Routing

When a user visits a webpage, the location is the the first (and probably the only) data the app has to generate a UI. The location is matched against a set of routes, a route is selected, (maybe) data is loaded, and finally the UI is rendered.

After the initial render on the client, a history listener is set up. When the history's location changes--either through the user clicking links or the programmer redirecting with code--the new location is matched against the routes, (maybe) data is fetched, and the app is updated to the new page.

Some implementations will save the data loading of a route for after the new page is rendered (and display some spinners). There are a handful of tradeoffs regarding data loading on page transitions, so we'll save that conversation for later.

Dynamic vs. Static Routing

There are two types of routing: dynamic and static.

Static Routing

If you’ve used Rails, Express, Ember, Angular, or React Router pre v4, you’ve used static routing. In these frameworks, you declare your routes as part of your app’s initialization before any rendering takes place. React Router pre-v4 was also static (mostly). Let’s take a look at how to configure routes in express:

// Express Style routing:
app.get("/", handleIndex);
app.get("/invoices", handleInvoices);
app.get("/invoices/:id", handleInvoice);
app.get("/invoices/:id/edit", handleInvoiceEdit);

app.listen();

Dynamic Routing

Dynamic Routing is when the routing happens as part of your app's render lifecycle. So instead of having a route configuration somewhere, you'll have some run-time version of it. In the case of React Router v4-5, you have a <Route> component that can be rendered anywhere and knows how to match the URL and renders almost like an if block.

Here's what it looks like in React Router:

const App = () => {
  return (
    <div className="layout">
      <GlobalNav/>
      <Route
        path="/matched/while/rendering"
        render={() => (
          <div>This only renders when matched</div>
        )}
      />
    </div>
  )
}

Tradeoffs

Dynamic Routing is great using application data for conditional route matching:

For example, code-splitting just naturally happens, since there is no route config at the top there is no need for a "code-splitting feature" in a dynamic router.

Another example is authentication. There's no need for a mechanism to "protect routes": you simply don't render those routes until the user is authenticated, if they're not authenticated, you just render a login screen instead (at every url, but you just don't have routes yet).

Additionally, if a small screen renders a significantly different UI than a large screen, you simply render different routes given the screen size data that you know at render time.

In general, the ability to add or remove routes as part of your normal render tree opens up a lot of potential features. Unfortunately most web developers and designers haven't really explored what we could be doing. We're all still very used to thinking about routing simply as "pages", the same way we've thought about them since the beginning of the web.

Static Routing is great for data loading:

If your routes live outside the render lifecycle, you're able to work out data requirements before rendering the next page, or even preload data before a link is even clicked. In general it gives you more control over transitions in the client and more comprehensive server pre-rendering. We'll talk more about this in the data loading section.

Static routes are also easier to see all of the URLs your app responds to at a glance, where dynamic routing is often spread across the app. However, with static routes you're likely to have git conflicts with other teams, increase the base bundle size, etc. where dynamic routes don't have these problems. Tradeoffs everywhere.

URL-As-Data vs. URL As-Side-Effect

Most routing developers are familiar with uses "URL as data". Meaning, we use the URL as data to match against to render the UI and navigation simply changes that URL to start over again from the top.

"URL as side effect" is another approach that treats the URL as nothing more than a side-effect, or reflection, of the app state.

A good analogy is the document title. When your app gets into a state where you want to change the document title, you go ahead and do it. URLAASE (lol) is the same. When your app reaches a state you think is worth a new URL, you push it up there.

Some complexity enters with this approach when you boot the app initially. Suddenly the URL is no longer a side-effect but the initial state of the app. This causes two code paths for each URL the app supports, opening up the risk of missing a case in one place but not the other. If you've ever done mobile development without a "URL as data" router, you know how difficult it can be to support every feature as a "deep link" into the app.

Routers don't need to pick one approach over the other. React Router is a "URL as data router" but it also supports treating the URL as a rendering target with the <Redirect> component. An example of using both approaches in the same app is a page that changes the URL as the scroll position changes. As the user scrolls, you can redirect to a new path that represents that position as a side-effect, but you'll also need to write the code to scroll down on both the initial load and user navigation.

Server Side Approaches

Before we can talk much about anything else, we should address the three server side approaches used to deliver the application to the browser.

  1. "Single Page App"
  2. Static File Generation
  3. Serverside Pre-Rendering

Server Side Approach: "Single Page App"

A traditional website handles URLs on the server. This can be as simple as an html file for each page of your site: /index.html, /contact.html etc. But in order for a client side router to work, every URL the server receives needs to run the same application. Or, in other words, every URL needs to serve the same html file.

This is why they've been called "Single Page Applications". There's really only a single index.html that handles every URL in the app, and then a router decides what UI to render with JavaScript in the browser.

The body of the html file is typically blank, with a single element for JavaScript (and your client side router) to mount an application into.

<html lang="en">
  <meta charset="utf-8" />
  <title>Bare Bones SPA</title>
  <body>
    <div id="root"></div>
    <script src="the/app.js"></script>
  </body>
</html>

No matter what the URL is, when if this file is served, then the router inside of app.js can read the URL and determine what page to display.

Here's a bare-bones express server to do just that:

const express = require('express');
const app = express();

// serve all static files that exist as the file (CSS, JS, etc.)
app.use(express.static('public'));

// serve every other request to your root index html file
app.get('*', res => {
  res.sendFile(path.join(__dirname, 'index.html'));
});

app.listen(5000);

Now if the user visits /invoices/123 they will get the index.html file and the client side router can decide what to render.

This is how create-react-app plus a router like React Router works.

Server Side Approach: Server Pre-Rendering

Server Side Approach: Static File Generation

In the early days of web development, before client side routing and even before server side routing, we just had html files. We just put html files into the public facing directory of our web servers and we had a "route".

A silly homepage public directory could look like this:

├── about.htm
├── contact.htm
├── index.htm
└── pics
    ├── IMG_2342.JPEG
    ├── IMG_3532.JPEG
    ├── cat.htm
    ├── dog.htm
    ├── index.htm
    ├── me.htm
    └── me.jpeg

This is why URLs look like a directory structure, because that's what they originally were!

Modern tools have continued to take this approach, but instead of writing separate html files for every page, you can create just the body of the page with JavaScript like its a Single Page app. At build time, the pages are compiled into full HTML files at the same paths where they were authored for the web server.

When a user visits, and an HTML file is loaded into the browser, the JavaScript app, along with a router, is loaded. Every navigation from now on acts exactly like a Single Page Application--no more round trips to the server for assets, just data.

It feels like the old days of just creating files to get a new "route" with all performance benefits of static file servers, except the transition from "plain html" to dynamic, interactive UI is seamless because it's the same code.

Gatsby.js works this way, and Next.js supports it (in addition to full server rendering).

Code Splitting and Bundle Size Concerns

Server Rendering

Data Loading

Something something about static is easier for data loading but I don't like coupling data to routes because there are so many interactions/component transitions that are data dependent (combobox, tabs, wizards, checkout workflows) but not necessarily routes. HOWEVER, maybe they should be routes, even if just using location state.

Something something about my hopes for suspense to bend all the tradeoffs here but my conversations w/ the React team make me doubt it will be the silver bullet I thought it was 18 months ago.

Clientside Page Transitions

Accessibility: Scroll Restoration

Accessibility: Focus Management

Animation

Dynamic Routes (Screen Size, Authentication, etc.)

Navigation Based Matching (instagram)

Relative Routes, Links

Navigation Blocking

Queries

History Stack

Mobile

File System API

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment