Remix Preview #1

Ryan FlorenceApril 30, 2020

Back in 2014 we created React Router. Since then it has become a staple piece of the React ecosystem. Over 1M people say they use it on GitHub 😮

Routing is at the heart of everything an application does, whether you're data loading, caching, or code-splitting, you're sure to bump into a routing API.

We’ve wanted to build this bigger piece on top of React Router pretty much since the beginning—now we're doing it.

Remix Logo, an R where the straight line is cut into three sections, looking like the notes in a digital audio workstation track, with inner and outer glow.

What is it?

Remix is a web application framework for React from the authors of React Router: Michael Jackson and Ryan Florence (me). It provides APIs and conventions for server rendering, data loading, routing and more. Everything we'd like a framework to do for us. Michael and I tend to build things to scratch our own itch. We've always envisioned a higher-level framework on top of React Router—that's Remix.

I imagine in the future a file system is enough to create a route config.

To that end, Remix is in the same category as Next.js and (to lesser extent) Gatsby. However, we're not optimizing for static site generation.

The kinds of products Michael and I have built in our careers, and the majority of projects we've seen from our clients at React Training (the company we’ve run together for the last five years), have highly dynamic data that can't be captured at build time, so we want to build a framework specifically for them (and us).

Video Walkthrough

We're going to take a look at some features of Remix in this email but if you want to go deeper check out this video. It covers the same things but I actually build the UI and go into even more details about each topic. If you've got 20 minutes--or a lot less depending on how much you speed up YouTube videos 😂

File System Routes

To create new route in Remix, you just add a file to the "routes/" folder. Simple as that. We've taken it a bit farther though since Remix is built on top of React Router. React Router has always been about "nested routes". Layout and URL nesting can be coupled, which is usually how your app is designed.

Screenshot of a filesystem showing files inside of a routes directory

Imagine you've go some UI that's got a navigation menu at the top and then different views that swap around below it.

You can click that, it's interactive.

This is a common UI pattern and it often nests one or two (or more!) levels. One part of the UI (the nav) persists while the stuff adjacent to it swaps out.

Start of example
Mini Example Application
Fake URL Bar
https://example.com/users/
Users

Welcome to our demo page, you’re looking at the index.
End of example

Your elements might look something like this.

Start of example
Code sample, App.js
Line 1, <App>
Line 2, <UsersLayout>
Line 3, {page === "index" && <UsersIndex />}
Line 4, {page === "user" && <User />}
Line 5, </UsersLayout>
Line 6, </App>
End of example

In React Router v6 (currently in alpha) it looks something like this.

Start of example
Code sample, App.js
Line 1, <App>
Line 2, <Routes>
Line 3, <Route path="users" element={<UsersLayout/>}>
Line 4, <Route path="/" element={<UsersIndex />} />
Line 5, <Route path=":user" element={<User />} />
Line 6, </Route>
Line 7, </Routes>
Line 8, </App>
End of example

Here's a little bit bigger route config.

Whenever a route nests inside another, the layouts also nest.

This is just React Router stuff, but it's what Remix's routing is built on top of.

Start of example
Code sample, App.js
Line 1, <App>
Line 2, <Routes>
Line 3, <Route path="/" element={<Index/>} />
Line 4, <Route path="gists" element={<Gists/>}>
Line 5, <Route path="/" element={<GistsIndex/>} />
Line 6, <Route path=":user" element={<GistsUser />} />
Line 7, </Route>
Line 8, <Route path="p" element={<Post/>}>
Line 9, <Route path="remix-preview" element={<RemixPreview/>} />
Line 10, <Route path="url-search-params" element={<UrlSearchParams />} />
Line 11, </Route>
Line 12, <Route path="*" element={<FourOhFour/>}/>
Line 13, </Routes>
Line 14, </App>
End of example

With Remix you get this kind of layout + route nesting by virtue of the file system

Start of example
File browser screenshot showing a nested folder structure with nested files inside, like routes/gists/$user.js and routes/gists.js
End of example

When the URL is "/gists", the UI hierarchy looks like this

Start of example
Code sample, /gists
Line 1, <App>
Line 2, <Gists> {/* gists.js */}
Line 3, <GistsIndex /> {/* gists/index.js */}
Line 4, </Gists>
Line 5, </App>
End of example

And when the URL is "/gists/mjackson"

Nested files become nested UI!

Start of example
Code sample, /gists/mjackson
Line 1, <App>
Line 2, <Gists> {/* gists.js */}
Line 3, highlighted, <GistsUser /> {/* gists/$user.js */}
Line 4, </Gists>
Line 5, </App>
End of example

Nested URLs without Nested UI

You may want a nested URL without layout nesting.

Remix supports this with dots in the file name that become slashes in the URL.

For example, gists.favorites.js.

Start of example
  • routes folder
    • gists folder
      • $user.js file
      • index.js file
    • gists.favorites.js file
    • gists.js file
    • index.js file
End of example

Here we can see the different UI nesting that matches the file nesting

If the user is at "gists/favorites" then the "gists.favorites.js" route will render, but since it's not nested inside of the "gists" folder, it's not inside of the Gists layout like the other routes we've looked at.

  • Nested Files == Nested UI
  • Flat Files == Flat UI
Start of example
Code sample, /gists/favorites
Line 1, <App>
Line 2, <GistsFavorites /> {/* gists.favorites.js */}
Line 3, </App>
Code sample, /gists/mjackson
Line 1, <App>
Line 2, <Gists> {/* gists.js */}
Line 3, <GistsUser /> {/* gists/$user.js */}
Line 4, </Gists>
Line 5, </App>
End of example

Dynamic Segments

As you've probably already guessed, files that start with $ like gists/$user.js create dynamic URL segments.

You can access them with React Router's useParams() hook as usual.

These parameters are typically used for data fetching, but we've got something even better in Remix coming up next.

Start of example
Code sample, routes/gists/$user.js
Line 1, import React from "react";
Line 2, import { useParams } from "react-router-dom";
Line 3,
Line 4, export default function GistsUser() {
Line 5, // given a URL of /gists/ryanflorence
Line 6, // params.user would be "ryanflorence"
Line 7, let params = useParams();
Line 8,
Line 9, return (
Line 10, <div>
Line 11, <h2>{params.user}’s Gists</h2>
Line 12, {/* ... */}
Line 13, </div>
Line 14, );
Line 15, }
End of example

Data Loading

Before a page renders you want to fetch the data. Michael and I have been through a lot of ideas here over the years since Routing is right in the thick of things when it comes to data loading.

Before I ever showed anybody my first version of React Router, I had a static method on Route components called getInitialData, and some code to fetch all of it before the page rendered (copying Ember's "model" hook at the time). In React Router v1-v3 we had <Route onEnter/> that got called when the route was about to be rendered.

The troubles with both of these approaches were handling error states, providing loading UI feedback, and then handling data mutations for that page. It always felt like this was something React itself needed to handle (not all fetched data happens on route transitions after all).

Enter Suspense. It provides APIs for us to load data on route transitions without all the hacks we had before. I love Suspense. It's the thing that really motivated me personally to build Remix, and execute on the ideas Michael and I have been jamming on in conversations for years.

Remember our route for a user's gists

Start of example
Code sample, gists/$user.js
Line 1, import React from "react";
Line 2, import { useParams } from "react-router-dom";
Line 3,
Line 4, export default function GistsUser() {
Line 5, let params = useParams();
Line 6, return (
Line 7, <div>
Line 8, <h2>{params.user}’s Gists</h2>
Line 9, {/* ... */}
Line 10, </div>
Line 11, );
Line 12, }
End of example

Without Remix, here are the changes we'd make to load up some data

We'd need to goof around with loading states for every route that fetches data, too. If you've got a 3-4 nested routes that's at least 4 different spinners 🙃

Start of example
Code sample, gists/$user.js
Line 1, highlighted, import React, { useState, useEffect } from "react";
Line 2, import { useParams } from "react-router-dom";
Line 3,
Line 4, export default function GistsUser() {
Line 5, let params = useParams();
Line 6, highlighted, let [gists, setGists] = useState();
Line 7,
Line 8, highlighted, useEffect(() => {
Line 9, highlighted, (async () => {
Line 10, highlighted, let api = "https://api.github.com";
Line 11, highlighted, let res = await fetch(`${api}/users/${params.user}/gists`);
Line 12, highlighted, let gists = await res.json();
Line 13, highlighted, setGists(gists)
Line 14, highlighted, })();
Line 15, highlighted, }, [params.user])
Line 16,
Line 17, return (
Line 18, <div>
Line 19, <h2>{params.user}’s Gists</h2>
Line 20, {/* ... */}
Line 21, </div>
Line 22, );
Line 23, }
End of example

Reset, let's try that again

Start of example
Code sample, gists/$user.js
Line 1, import React from "react";
Line 2, import { useParams } from "react-router-dom";
Line 3,
Line 4, export default function GistsUser() {
Line 5, let params = useParams();
Line 6, return (
Line 7, <div>
Line 8, <h2>{params.user}’s Gists</h2>
Line 9, {/* ... */}
Line 10, </div>
Line 11, );
Line 12, }
End of example

Export an async load function

Remix will call this function for all of the routes that match a location.

On the server Remix will wait for all data to load before streaming the markup down. (Fetch-then-render if you're privvy to that vocabulary.)

In the browser's initial render, it doesn't fetch anything, the server hands off what it loaded.

Subsequent route transitions in the browser will call this load function and send it to React Suspense to orchestrate the loading states. (Render-as-you-fetch, again, if you're privvy, but not going to get into that in this article 😅)

Start of example
Code sample, gists/$user.js
Line 1, import React from "react";
Line 2, import { useParams } from "react-router-dom";
Line 3,
Line 4, highlighted, export async function load() {
Line 5, highlighted,
Line 6, highlighted, }
Line 7,
Line 8, export default function GistsUser() {
Line 9, let params = useParams();
Line 10, return (
Line 11, <div>
Line 12, <h2>{params.user}’s Gists</h2>
Line 13, {/* ... */}
Line 14, </div>
Line 15, );
Line 16, }
End of example

Read the params

We're going to need them to load the gists.

Start of example
Code sample, gists/$user.js
Line 1, import React from "react";
Line 2, import { useParams } from "react-router-dom";
Line 3,
Line 4, highlighted, export async function load({ params }) {
Line 5,
Line 6, }
Line 7,
Line 8, export default function GistsUser() {
Line 9, let params = useParams();
Line 10, return (
Line 11, <div>
Line 12, <h2>{params.user}’s Gists</h2>
Line 13, {/* ... */}
Line 14, </div>
Line 15, );
Line 16, }
End of example

Return the data

Instead of goofing around with useEffect we get to write just a plain old function that returns a value.

Remix will know to reload data when locations change.

Start of example
Code sample, gists/$user.js
Line 1, import React from "react";
Line 2, import { useParams } from "react-router-dom";
Line 3,
Line 4, export async function load({ params }) {
Line 5, highlighted, let api = "https://api.github.com";
Line 6, highlighted, let res = await fetch(`${api}/users/${params.user}/gists`);
Line 7, highlighted, let gists = await res.json();
Line 8, highlighted, return { gists }
Line 9, }
Line 10,
Line 11, export default function GistsUser() {
Line 12, let params = useParams();
Line 13, return (
Line 14, <div>
Line 15, <h2>{params.user}’s Gists</h2>
Line 16, {/* ... */}
Line 17, </div>
Line 18, );
Line 19, }
End of example

Access the Data

The data you return goes to the Remix cache and can be accessed with the useRouteData hook. Super easy.

Now we could iterate data.gists and away we go!

Since it uses Suspense, we get to use Suspense's loading fallback APIs instead of requiring a loading state in every place we load data. We'll dig deeper into that another time.

Start of example
Code sample, gists/$user.js
Line 1, import React from "react";
Line 2, import { useParams } from "react-router-dom";
Line 3, highlighted, import { useRouteData } from "remix";
Line 4,
Line 5, export async function load({ params }) {
Line 6, let api = "https://api.github.com";
Line 7, let res = await fetch(`${api}/users/${params.user}/gists`);
Line 8, let gists = await res.json();
Line 9, return { gists }
Line 10, }
Line 11,
Line 12, export default function GistsUser() {
Line 13, let params = useParams();
Line 14, highlighted, let data = useRouteData();
Line 15, return (
Line 16, <div>
Line 17, <h2>{params.user}’s Gists</h2>
Line 18, {/* ... */}
Line 19, </div>
Line 20, );
Line 21, }
End of example

Location-Based Cache

All of this is backed by a suspense cache. That means when the user clicks "back" or "forward" in the browser we don't need to refetch anything. What's really interesting though is the cache isn't keyed off of just the route but rather by route + location. You can be at the same route but two different locations. Lemme 'splain.

Let's say a user is on Page A

Start of example
End of example

They click a link Page B

Remix will fetch the data for Page B now and render it.

Start of example
End of example

They click another link taking them to Page A

Note that they did not click the browser back button. They clicked on a link.

In this case, Remix will fetch the data for Page A again, even though it fetched it a couple minutes ago.

Remix caches data by route + location, not just routes. Routes and locations are not the same thing.

As we can see here, we have two separate locations for the same route.

Start of example
End of example

They click the browser "back button"

In this case, Remix will not call the route load function to load data for Page B. It will simply use the cache.

Start of example
End of example

Route-based caching

Of course, there are use cases where you want the data to be based solely on the route, or to expire the cache for all locations of a route, and we've got ways to do that, too.

For example, maybe an action on the second Page A changes the data, and if the user clicks back to the other Page A it's important to show them the updated information.

Start of example
End of example

The default is location based caching

By default, our cache works the way browsers work without JavaScript at all: click back and you see exactly what you saw before.

Start of example
End of example

When the user clicks back, even if you set cache headers on the HTTP response, browsers will ignore them 😂

History mechanisms and caches are different. In particular history mechanisms SHOULD NOT try to show a semantically transparent view of the current state of a resource. Rather, a history mechanism is meant to show exactly what the user saw at the time when the resource was retrieved.

By default, an expiration time does not apply to history mechanisms. If the entity is still in storage, a history mechanism SHOULD display it even if the entity has expired, unless the user has specifically configured the agent to refresh expired history documents.

HTTP/1.1 Section 13.13

I've personally edited a document online and realized I accidentally deleted an entire section of it! I clicked back and saw exactly what I saw before, copied the text, edited, and put the text back. Caches and history mechanisms are different 😉

I'm really excited about a cache built on top of locations because you can keep data fetching code super light in your Remix app. Just some plan ol' fetches will work really, really well. However, Remix's location-based cache will work great with other data abstractions like Apollo, Relay, react-query, and others that maintain their own cache.

Meta Tags and Document Titles

No website is complete if it doesn't change the title on navigation or provide meta tags for social embeds and SEO.

This stuff pretty much always depends on the dynamic data the component loads. So, we've built meta tags right into routes at the data loading layer instead of adding more abstractions. In yourload export, return a meta key and Remix will dump that stuff into the document. That's it. Check it out:

Line 1, export async function load({ params }) {
Line 2, let api = "https://api.github.com"
Line 3, let res = await fetch(`${api}/${params.user}/gists`);
Line 4, let gists = await res.json();
Line 5, return {
Line 6, gists,
Line 7, highlighted, meta: {
Line 8, highlighted, title: `${gists.length} gists from ${params.user}`,
Line 9, highlighted, description: `Check out all of ${params.user}'s gists!`,
Line 10, highlighted, "og:image": gists[0].owner.avatar_url
Line 11, highlighted, }
Line 12, };
Line 13, }

When the user navigates the title will automatically be updated, and when server rendered, the meta tags will be ready for the search engine bot or the social embed 🎉

We're still bikeshedding this API a bit, hit us up on twitter with your preference if you feel strongly about it:

Line 1, export async function load({ params }) {
Line 2, let { user } = params
Line 3, let api = "https://api.github.com"
Line 4, let res = await fetch(`${api}/${user}/gists`);
Line 5, let gists = await res.json();
Line 6, return { gists }
Line 7, }
Line 8,
Line 9, // Maybe a another exported function
Line 10, highlighted, // instead of a magic key on the data
Line 11, highlighted, export function head({ data, params }) {
Line 12, highlighted, let { gists } = data
Line 13, highlighted, let { user } = params
Line 14, highlighted, return {
Line 15, highlighted, title: `${gists.length} gists by ${user}`,
Line 16, highlighted, description: `Check out ${user}'s gists!`,
Line 17, highlighted, "og:image": gists[0].owner.avatar_url
Line 18, }
Line 19, }

Pricing

Michael and I have done a lot of open source and we absolutely love it. The core of Remix is React Router, which is free and always will be. Remix, however, is going to be a paid product.

We want to put everything we’ve got into this and support the web development community with great software for many years to come. We haven't completely worked out all the details but here's what we've got right now:

  • Affordable Indie License: Perpetual license + two years of free updates for one person. We'll build the framework so you can focus on your side-project. All we ask is you take Michael and I out to a fancy dinner—well, the financial equivalent of such 🍽
  • Commercial License: Perpetual license for a development team + annual subscription for updates and support.

Our customers will also have access to the GitHub repo to report issues, comment on API proposals, even send PRs with bugfixes if you'd like to scratch your own itch!


That’s it! Thanks for your interest and support in Remix, we're confident you're going to love it. We’ll continue to keep you posted in the newsletter and here on the blog.

Subscribe to our mailing list

Subscribe to our newsletter to follow our progress and get early access to Remix

We respect your privacy, unsubscribe at any time

Copyright © 2020 Remix