Skip to content

Instantly share code, notes, and snippets.

@ryanflorence
Last active September 1, 2021 12:35
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ryanflorence/0d62d4fa42799f3b92f847f5849381a3 to your computer and use it in GitHub Desktop.
Save ryanflorence/0d62d4fa42799f3b92f847f5849381a3 to your computer and use it in GitHub Desktop.

Route Config

I think I just figured out two major things I wanted to work out:

  1. Not sending the entire route config to the client, only the ones that matched and that are linked to on the page.

  2. Defining routes manually in replace of, or in addition to, the conventional file-system routes

The reason I've been hesitant on (2) is because of (1). Take a highly interactive blog where each article is a full blown React component like our blog instead of just markdown you can lookup and parse on the server.

If it's got 100,000 or even 100 articles it's kind of a non-starter. Every page (right now in Remix) sends down the entire route config, which would just flat out be too large for a website with 1,000 pages. That initial bundle and render is just silly.

But I think I just figured it out.

Not sending the entire route config

Right now we have the webpack plugin that generates the virtual routes.js module. It generates a module that looks something like this:

import React, { useMemo } from "react"
import { useRoutes } from "react-router-dom"
import { remixRoute, useRoutesCache } from "remix"

// Every single route has a component and it is included here
const CourseIdChapterId = remixRoute(
  () => import("./routes/$courseId.$chapterId.js"),
  "$courseId.$chapterId"
)
const CourseId = remixRoute(() => import("./routes/$courseId.js"), "$courseId")
const Index = remixRoute(() => import("./routes/index.js"), "index")

export default function RemixRoutes() {
  let cache = useRoutesCache()

  // and then again they are all used here in routes
  let routes = useMemo(
    () => [
      {
        path: ":courseId/:chapterId",
        element: <CourseIdChapterId />,
        preload: (...args) => CourseIdChapterId.preload(cache, ...args)
      },
      {
        path: ":courseId",
        element: <CourseId />,
        preload: (...args) => CourseId.preload(cache, ...args)
      },
      {
        path: "/",
        element: <Index />,
        preload: (...args) => Index.preload(cache, ...args)
      }
    ],
    [cache]
  )
  return useRoutes(routes)
}

The server already knows all of the routes, and the client only needs the matched routes to render initially. So, we generate something like this file on the server per request instead of bundling up every possible route in the build. We don't need every route on the first render.

(Right now webpack creates the code-split bundles because of this file. So if we generate this information server-side we'll need to add every route as an "entry" during the webpack build.)

But now the question is, how do we get React Router to match the next route when a user clicks a link if it's not in the useRoutes config?

We ask the server, of course!

Remix Link

We can have a special Remix link that makes a request to the server with it's to value when mounted, the server matches that against the routes on the server, and sends the partial route config back to the client, and we update our useRoutes at the top to include it.

Even better, if multiple Links show up on the page at the same time we can send all of their to values in one request, and patch useRoutes with all of the routes that we can possibly transition to. Additionally, this would be a good time to preload the data for that link as well (we'd want to stream the route config down first, then stream the data).

Remix Navigate

navigate(to) is a bit trickier since it lives inside button/form event handlers. We can't know them ahead of time. To solve these we can do two things:

  1. We have a special remix useNavigate(path). They pass in a route path (not a url) and it will do the same thing as Links, going to the server and getting the extra route config before the transition ever happens.

  2. We can have a special "no match" in remix in the client. When the user navigates somewhere that is a no-match we simply do a window.location = to.

    • This way, if there is actually a match and they just forgot to useNavigate(path) they'll get the page.
    • If there's no page there, the browser gets the proper 404 status
    • If the server has redirects set up, the redirect will happen properly (with 30x) and they'll get the right page.

Manually creating routes

Because we now have a solution to not sending every route to the client, I'm excited about manually generating routes. This will allow us to have folders like "articles/" and "pages/" full of markdown or MDX or whatever. This will be much simpler than screwing around with the "routes/" folder for "static content".

Imagine a file at the root of the application repo named routes.js that looks like this:

import { promises as fs } from "fs"
import { conventionalRoutes } from "remix/build"

export default async function(route) {

  // get all the paths for your markdown
  let [articles, pages] = await Promise.all([
    fs.readdir("src/articles"),
    fs.readdir("src/pages")
  ])

  // still use conventional routes, too
  conventionalRoutes(route)

  // maybe the "inbox" team wants to define their routes manually
  // and have a folder called "inbox" that nobody else deals with
  // and they want their "loaders" all in a sub folder, too
  route("inbox", "inbox/Messages.js", "inbox/loaders/messages.js" () => {
    route(":message", "inbox/Message.js", "inbox/loaders/message.js")
    route("archive", "inbox/Archive.js", "inbox/loaders/archive.js")
  })

  // here we can generate every route for every blog article we've got
  route("blog", "blog/Blog.js", () => {
    route("/", "blog/BlogIndex.js")
    for (let path of articles) {
      route(path.replace(/\.md$/, ""), path)
    }
  })

  // and finally genrate any "pages"
  for (let path in pages) {
    route(path.replace(/\.md$/, ""), path)
  }
}

We'd also be able to clean up the kind of whacky code that reads the routes/ folder right now to just use this API itself.

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