Trying Out Remix

trying-out

I came across Supporting Remix with full stack Cloudflare Pages on Cloudflare blog (yes I’m a CF fan) and Remix piqued my interest. Mostly because it addresses my concerns with “modern” web development: the wiring boilerplate is mostly gone, there is no 100 file setup to get a workable dev environment. And of course because it’s outside my comfort zone - so maybe I’ll learn something.

Getting started

There’s a code example right on the home page but I’m having hard time building a mental model from just that. Luckily there is also a bit Get started button which takes me to a tutorial. Great, let’s follow that.

following initial instructions

Few more commands

1
2
cd my-remix-app
npm run dev

And we’re up.

localhost shows dmeo app

Then there is text that really speaks to me

If you want, take a minute and poke around the starter template, there’s a lot of information in there.

Oh yes I intend to do that πŸ˜„

Let’s make a git repo to keep track of changes. Conveniently there is aready a gitignore.

1
2
3
git init
git add .
git commit -m "Initial commit"

Let’s now see what we have

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
 git ls-files
.gitignore
README.md
app/entry.client.tsx
app/entry.server.tsx
app/root.tsx
app/routes/index.tsx
package-lock.json
package.json
public/favicon.ico
remix.config.js
remix.env.d.ts
tsconfig.json

Honestly less than I expected. And I mean this in the best possible way. I like environments which I can understand to the point of being able to write manually from scratch. Yest I’m looking at you create-react-app and other humongous frontend toolchains. I feel at home with Go where you have your dependencies file and a single go command.

Back to source files; there’s a typescript config but no webpack config (yay?) or similar. This is in the typescript config

1
// Remix takes care of building everything in `remix build`.

Great! Let’s find the entrypoint now to better understand this.

Since I’ve been instructed to use npm run dev this means that package.json will define the action.

1
2
3
4
5
6
  "scripts": {
    "build": "remix build",
    "dev": "remix dev",
    "postinstall": "remix setup node",
    "start": "remix-serve build"
  },

And it does, no magic here.

There is something the looks like remix configuration (remix.config.js)

1
2
3
4
5
6
7
8
module.exports = {
  appDirectory: "app",
  assetsBuildDirectory: "public/build",
  publicPath: "/build/",
  serverBuildDirectory: "build",
  devServerPort: 8002,
  ignoredRouteFiles: [".*"]
};

And apparently it points to app directory. Conveniently there I can find entry.client.tsx and entry.server.tsx which I guess are my entrypoints. What I found slightly strange is .tsx (which I believe is JSX for typescript) for the server.

Maybe time to read some more docs 🀷

Baby’s first code

localhost shows dmeo app

Confused for a moment…apparently docs are not up to date, the links are now in app/routes/index.tsx. Oh well, I’ll manage. I added the li as instructed

1
2
3
<li>
  <Link to="/posts">Posts</Link>
</li>

and then of course it fails to compile as Link is not defined. Guessed the import based on imports in root.tsx to be

1
import { Link } from "remix";

It works!

link to posts

Then created app/routes/posts/index.tsx as

1
2
3
4
5
6
7
export default function Posts() {
  return (
    <div>
      <h1>Posts</h1>
    </div>
  );
}

And non-surprisingly it shows up now. Time for some remix magic now.

Loaders

If your web dev background is primarily in the last few years, you’re probably used to creating two things here: an API route to provide data and a frontend component that consumes it. In Remix your frontend component is also its own API route and it already knows how to talk to itself on the server from the browser. That is, you don’t have to fetch it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { Link, useLoaderData } from "remix";

export const loader = () => {
  return [
    {
      slug: "my-first-post",
      title: "My First Post"
    },
    {
      slug: "90s-mixtape",
      title: "A Mixtape I Made Just For You"
    }
  ];
};

export default function Posts() {
  const posts = useLoaderData();
  return (
    <div>
      <h1>Posts</h1>
      <ul>
        {posts.map(post => (
          <li key={post.slug}>
            <Link to={post.slug}>{post.title}</Link>
          </li>
        ))}
      </ul>
    </div>
  );
}

And it show up as expected.

link to posts

Then the tutorial moves on to refactoring, extracting data and fetching from an external data source but I’m more interested in this mechanics as it seems to me this is the meat and the potatoes of remix. So what is going on?

Well apparently the useLoaderData hook is automatically tied to corresponding exported loader in the same route. By convention instead of us manually wiring up routing, neat. But I have questions now πŸ˜„ Can I pass parameters? Is there useQuery-like magical caching and deduplication? Authentication? CORS?

More interestingly: if I looks at network in devtools I don’t see an XHR request, meaning our data is pre-rendered on the server. Super neat! But if i navigate from index to posts I see a request to http://localhost:3000/posts?_data=routes%2Fposts%2Findex which directly returns my data.

Routes

Reading on the tutorial actually explains how to parametrize aka how to do dynamic route params.

It’s again done by convention, not explicit config: file name is a placeholder for a parameter. Clever?

I’ll admit my knee jerk reaction to this is “ugh, ugly. I want my router”. But it may work? I don’t know, would actually need to do a larger project to evaluate properly. In any case there is a way to get an overview of the routes:

1
2
3
4
5
6
7
8
$ remix routes
<Routes>
  <Route file="root.tsx">
    <Route path="posts/:slug" file="routes/posts/$slug.tsx" />
    <Route path="posts" index file="routes/posts/index.tsx" />
    <Route index file="routes/index.tsx" />
  </Route>
</Routes>

So I can still have my cake (easily overview and discover routes) and eat it (not write the router) too.

Ok, so how does this paramterized component look like?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import { useLoaderData } from "remix";

export const loader = async ({ params }) => {
  return params.slug;
};

export default function PostSlug() {
  const slug = useLoaderData();
  return (
    <div>
      <h1>Some Post: {slug}</h1>
    </div>
  );
}

And sure enough I can now click on one of the posts and it renders

rendered post

So what is happening here?

Now the analogy from the tutorial that loader is the controller and we’re using react as a view layer makes sense πŸ˜€

Then I followed the instructions to get markdown rendering up an running which is well covered in the tutorial and standard javascript so I’ll skip the details.

An experiment

But meanwhile I started wondering…can I put multiple components on screen and each will automagically fetch it’s data? I could peruse the docs some more…or I can just try it out.

I created this app/routes/footer.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import { useLoaderData } from "remix";

export const loader = async () => {
  return new Date().getFullYear();
};

export default function Footer() {
  const year = useLoaderData();
  return (
    <div>
      All rights reserved Β© { year }
    </div>
  )
}

Yes, very silly, calling a server to get current year, but I just want to test things out πŸ˜…

One remark while I figure things out: tooling is quite responsive and helpful: change detection, auto-recompilation and reloading out of the box.

1
2
3
4
5
6
7
8
Build failed with 1 error:
route-module:/Users/andrazbajt/personal/playground/my-remix-app/app/routes/posts/$slug.tsx:3:9: error: No matching export in "app/routes/footer.tsx" for import "Footer"
?? Rebuilt in 43ms
GET /posts/ 200 - - 19.486 ms
?? File changed: app/routes/posts/$slug.tsx
?? Rebuilding...
?? Rebuilt in 124ms
GET /posts/ 200 - - 11.870 ms

Then in $slug.tsg

1
2
3
4
<div>
  <div dangerouslySetInnerHTML={{ __html: post.html }} />
  <Footer/>
</div>

And a cryptic error appears

application error

What is slug doing here? Sprinkling in some logging I notice that year actually holds data from $slug.txs loader, not my footer loader. So I’m holding it wrong - back to the docs it is.

Styles

Next interesting bit is that components and also include styles

1
2
3
4
5
import adminStyles from "~/styles/admin.css";

export const links = () => {
  return [{ rel: "stylesheet", href: adminStyles }];
};

and this gets picked up by Links component in index.tsx.

Index routes

But the real fun begins with index routes. By adding this to app/routes/admin.tsx

1
2
3
4
5
import { Outlet } from "remix";
...
<main>
  <Outlet/>
</main>

things can now render inside this component. E.g. visiting /admin/new will render first app/routes/admin.tsx but inside the Outlet there will be /app/routes/admin/new.tsx. This time with working data fetching πŸ˜‰ But does this mean I can only structure my data-fetching components hierarchically?

Reading on I’m slightly surprised to see that remix includes its own forms. How do they differ from plain old html <form>? I’m guessing some automagical wiring to the backend, maybe shared validation logic 🀞

And sure enough, there is wiring by convention

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import { redirect, Form } from "remix";
import { createPost } from "~/post";

export const action = async ({ request }) => {
  const formData = await request.formData();

  const title = formData.get("title");
  const slug = formData.get("slug");
  const markdown = formData.get("markdown");

  await createPost({ title, slug, markdown });

  return redirect("/admin");
};

So what happens when I submit this form? Feels very smooth, let’s look under the cover.

Other stuff

Then there is another much longer tutorial (also available as a video) that dives deeper and covers more topics (like cookies, authentication, validation, error handling, databases…).

How about deployment? I did start this with the intent of deploying to Cloudflare but the post is already getting long and the app as-is is not really suitable (direct filesystem access is not supported) for deploying, so maybe in another post.

Conclusion

We’ll as much as I’m not a home at writing frontend code and I dislike the idea of “full stack javascript” this actually did not hurt a bit and I can see how time can be really useful for doing a very dynamic client app that as some server side components.

Now, I would probably not pick this stack for a new project, mostly to me not being comfortable with Node, but I might just use it for an opinionated React toolchain with SSR and all other goodies.


Last modified on 2021-12-19

Previous Running amd64 docker images with Podman on Apple Silicon (M1)
Next Speed is a feature; thoughts on modern web performance