I think I just figured out two major things I wanted to work out:
-
Not sending the entire route config to the client, only the ones that matched and that are linked to on the page.
-
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.
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!
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).
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:
-
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. -
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.
- This way, if there is actually a match and they just forgot to
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.