So, Remix made server-side rendering in JavaScript-land a thing again; who would have thought?
I wrote an article about doing it with Preact, but it was a bit of fiddling around because, well, I'm not one of the most skilled developers out there.
Anyway, Remix is out in the open and pretty rad. All my "old school" web knowledge from before SPAs is hip again. And the best thing is, it works serverless with solutions like Cloudflare Pages!
Infinite scale, edge located, on-demand payment? This is awesome!
So, let's take a look at how to set things up. The stack I'm starting with is:
Remix does server and client-side rendering, and Tailwind seems the best option here. I tried Chakra, AntD, and Blueprint without much luck.
For production systems, where I had to hand-roll CSS from Sketch files, I would use inline styles (CSS-in-JS), and Tailwind seems to be the logical evolution of this, without the need for JS. The utility classes are more potent than inline styles, but I can use them in the same place.
Tailwind 3 has a bundler that only includes used styles inferred from your JS/TS files, so you usually end up with <20KB of CSS.
Chakra and Blueprint use CSS-in-JS and, in turn, will generate JS. Currently, this requires rather much boilerplate code to get it working, and if I understood it correctly, it would need two render passes in the end, so not the best solution anyway.
AntD uses one big (>500kb) CSS file or some kind of weird babel transform to convert your component imports to component/CSS import.
While all the Tailwind classes don't seem clean, at least the build process is.
Storybook is a must here because all the UI kits I found based on Tailwind are ... well ... a bit heavy on the CSS classes side of things, haha. They don't provide React components out of the box, but only many HTML elements, so you have to create your React components yourself. Storybook really helps with that process.
Cloudflare Pages, because I like Cloudflare Workers, and Pages comes with a bit of deployment help on top. It's just a higher-level service built on Workers anyway. And, as I said, I'm not really the low-level dev.
So, let's dive in!
Prerequisites
We're using Node.js 16 for this, a Cloudflare and a GitHub account.
Setup
First, we initialize the Remix project.
$ npx create-remix@latest example
$ cd example
Choose "Cloudflare Pages" as the deployment target. This will set up the project with Wrangler, Cloudflare's CLI dev-tool. Also, TypeScript is the way!
Then, we add all packages required for Tailwind:
$ npm add -D tailwindcss postcss autoprefixer
Initializing Tailwind is the next step; as I said, version 3 uses a bundler that generates small CSS on the fly.
$ npx tailwindcss init -p
Storybook setup is next:
$ npx sb init
Configuring Tailwind
The tailwind.config.js
file should look like this:
module.exports = {
content: ["./app/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [],
}
When the Tailwind bundler runs, it creates its CSS file at app/styles/app.css
, so we need to import that file in the app/root.tsx
file.
import styles from "~/styles/app.css"
export const links: LinksFunction = () => [{ rel: "stylesheet", href: styles }]
Remix allows every component and page (pages are components too) to export a links function that can return <link>
elements which are added to the <head>
. Since we have all our styles in one CSS file, we can export it to the root element of our application.
The Tailwind bundler also needs a base CSS file at styles/app.css
(yes, this doesn't go into the app directory!) containing these three lines:
@tailwind base;
@tailwind components;
@tailwind utilities;
We need to update the scripts in package.json
to run the Tailwind bundler alongside the wrangler CLI.
"scripts": {
"build": "cross-env NODE_ENV=production npm run build:css && remix build",
"build:css": "tailwindcss -m -i ./styles/app.css -o app/styles/app.css",
"dev": "cross-env NODE_ENV=development run-p dev:*",
"postinstall": "remix setup cloudflare-pages",
"dev:remix": "remix watch",
"dev:wrangler": "wrangler pages dev ./public --watch ./build",
"dev:tailwind": "tailwindcss -w -i ./styles/app.css -o app/styles/app.css",
"start": "npm run dev:wrangler",
"dev:storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook"
}
I used the scripts generated from Remix and Storybook as a base, including scripts for Wrangler and Storybook.
The run-p
package allows us to run multiple scripts in parallel; Remix installed it when we initialized the project with the Cloudflare deployment target.
With the Remix and Wrangler CLIs, we already had two. So we just need to add one for tailwind and storybook with the correct dev:
prefix, so they get all started simultaneously.
This way, CSS gets bundled up with every change, and we can see it in the Remix app AND the Storybook right away!
As you can see, it uses the CSS file in styles
as a base and creates a bundled-up version in app/styles
as output.
Configuring Storybook
Let's get Storybook working with Tailwind!
First, we need to import the CSS file Tailwind bundled into Storybook. This can be done with an import in .storybook/preview.js
import "../app/styles/app.css"
Then we need a Tailwind-based component to test if everything is working. For this, let's create a Button
at app/components/button.tsx
.
import { MouseEventHandler } from "react"
export interface ButtonProps {
onClick?: MouseEventHandler<HTMLButtonElement>
text: string
}
export default function Button(props: ButtonProps) {
return (
<button
className="bg-slate-500 text-gray-50 rounded p-2"
onClick={props.onClick}
>
{props.text}
</button>
)
}
We can use this component in our Remix app in the app/routes/index.tsx
file if we're already at it.
import Button from "~/components/button"
export default function Index() {
return <Button text="Hello, world!" />
}
We also need a stories file to display that button inside Storybook. Let's create one at stories/button.stories.tsx
.
import { ComponentStory, ComponentMeta } from "@storybook/react"
import Button from "../app/components/button"
export default {
title: "Button",
component: Button,
} as ComponentMeta<typeof Button>
export const Regular: ComponentStory<typeof Button> = () => (
<Button text="Hello!" />
)
Running the Development Servers
Let's test everything by running all the dev servers.
$ npm run dev
This will start the Remix server, the Tailwind bundler, and the Storybook server. The HTTP servers endpoints should be opened in the browser automatically after the servers are up; Storybook takes a bit longer here.
In one tab, we see how our Button
integrates with the Remix application; in the other, we see it in the Storybook application.
Deploying to Cloudflare Pages
Now, to the serverless part of things!
To deploy the whole shebang to Cloudflare Pages, we need a GitHub repository. Cloudflare will watch for commits to that repository to trigger its deployments.
To create a new repo at GitHub, got to repo.new.
Then, initialize one repo locally, commit all files, link the local repo it up with your GitHub, and push the changes.
$ git init
$ git add -A
$ git commit -m "Init"
$ git remote add origin git@github.com:>GITHUB_USERNAME>/<GITHUB_REPO_NAME>.git
$ git branch -M main
$ git push -u origin main
After that, Cloudflare Pages can use our repository to deploy our app. Go to the dashboard to set things up.
On the sidebar on the left is a "Page" link where you can set up a new project.
Choose your GitHub project and the "Remix" framework.
We also need to set an environment variable, so the correct Node.js version is used by Cloudflare Pages to run Remix' build command.
NODE_VERSION=v16.7.0
After all is set up correctly, we just have to wait for the deployment to finish. This can take a few minutes.
In the end, Cloudflare Pages presents you with a link to your freshly deployed serverless side rendered Remix application.
When we throttle the network in the dev-tools to slow 3G, we should see around 2 seconds for the HTML and 2 seconds for the CSS to load.
If we disabled JavaScript, all interaction is handled inside a Cloudflare Worker, so nothing more has to be downloaded.
If we enabled it, things get a bit more sluggish on the first visit, since all that JS has to be downloaded. But at least on my machine this is still takes only around 5 seconds.
Conclusion
Setting up this new and exciting stack isn't a big deal. Remix allows for easy CSS integration, and Tailwind generates a basic CSS file, a match made in heaven!
Storybook is a good addition since Tailwind is more on the hand-rolled side of things. This gives you flexibility, but you also need a way to check if everything works right!