Last week we discussed how to initialize and use Remix. And, frankly, you can go and create a great app just with this knowledge. But as always, I like to go a bit further, especially since I am using these things in my daily work, and have my own starter.

Introduction

Remix uses two kinds of starters: regular templates and stacks. The first are rather reserved for themes with minimum opinionated addons, the latter – quite the oposite. My here is Slowcore Stack. I am throwing a lot of my own thoughts and discoveries in here, along with my favorite patterns. If you’re interested, let’s go!

Remix convention is to name stacks using music genres. You can find blues stack, indie stack (with Sonic Youth on the cover, woo!) and grunge stack, which are authored by the Remix team, and much more that are community-made, like this one.

What is important to note is that a lot of stacks are geared towards serving as both the fully-fledged backend, and frontend together. So you might get database configuration and connection out of the way, deployment strategies and all these things. It’s not the case here. I am using different backends, sometimes with REST, sometimes with GraphQL, sometimes file-based and I don’t want my frontend to know all about it.

That’s why Slowcore Stack is more of a backend-for-frontend kind of solution. It utilizes the server to do all the rendering and data transactions, but it only communicated with APIs. This makes it very lightweight and cheap in maintenance. And gives you more freedom: you want to use Firebase? Go for it! Want to a headless CMS that you’ve just discovered? Sure thing! Want to serve from flat .md files? You can do that as well.

Okay, so with that out of the way, let’s jump to the actual code and see how it handles.

Creating a Remix app using a stack

Remix makes it really easy to create a new app using a predefined template. It has built-in support for Github as well, so all you need to do, is use:

~ npx create-remix@latest --template github-username/repo-name;
# or
~ pnpm dlx create-remix@latest --template github-username/repo-name;

You can also source local files, replacing github-username/repo-name with a directory name!

Right, so let’s kick start by creating a new app using the Slowcore Stack:

~ pnpm dlx create-remix@latest --template tomekbuszewski/slowcore-stack;

I will be using PNPM for the rest of this video, but you’re free to use whatever you like.

This now asks the normal questions, but in the end, it will ask whether we want to run remix.init script. This is a hard yes. This script will configure everything accordingly, like pick the correct package manager and adjust package scripts.

First of all, there’s a README file with some mandatory music to listen to. I was even going to make a template and name it after the band Karate, but then I though to expand the scope a bit. And that’s how the slowcore stack has been born.

The gist

Slowcore comes with a lot of things predefined. It has:

  • formatting with Prettier, enforcing double quotes, semicolons an trailing commas;
  • linting with ESLint, with a rather standard setup, throwing in some hooks recommendations and import sorting;
  • TypeScript, to have everything more-or-less type-safe;
  • Storybook for developing UI in isolation;
  • tests:
    • unit and integration tests are written with Vitest;
    • E2E tests are written using Playwright.
  • renovate to make sure all the packages are up-to-date;
  • styling with Tailwind, as it also comes with Remix by default;
  • .env validation against .env.example;
  • Plop generator for scaffolding UI (with Atomic Design principles), hooks and features….

Fun fact: I’ve initialized this project using BiomeJS instead of Prettier/ESLint combo, but it turned out to be a bit of a resource hog (in IntelliJ, maybe it’s better in VSC), so I reverted back.

Right, so let’s dive a bit deeper and see some details. And I will not discuss minor things like Prettier, TS or linting, simply to save you some time.

UI Generation

If you saw my video on Atomic Design for React, you’ll feel like home here. I am following the same principles, to the point of carrying over a lot of files from that project. Basically, by running

~ pnpm run plop

you’ll get a prompt asking what you want to generate. Picking ui is followed by asking whether this should be an atom, a molecule or an organism. And that’s it, you pass the name and get the component, types, story and tests for it out of the box! Just remember that this is no magic and you’ll have to fill some of the blanks, as it comes with bare minimum.

Hooks generation

Generating hooks is very similar to generating UI, as it is also conducted by Plop. It will create a new directory in app/hooks with your hook name (formatting it properly, so if you name it use something something, it will turn it into useSomethingSomething) and will export it using barrel imports in app/hooks/index.ts.

Feature generation

Making features is also as easy. It generates the directory in app/features with action, loader, types, tests and the component itself. More on this later!

Testing

As said earlier, Slowcore comes with Vitest for smaller tests, and Playwright for fully-fledged E2E experience. It takes care of running the server as well, so all you want is to run pnpm run test:e2e to get it running.

Tests also come with MSW baked-in, so you can forget about intercepting requests and mocking your fetches. You just create the entire mock of your API (writable if you want!) and let it run!

ENV validation

This is something I’ve been meaning to do for a long time. How many times did your build step failed simply because you forgot to define an environment variable? Well, say goodbye to this. Slowcore comes with a check that verifies if all of the fields defined in .env.example are available in the environment. They don’t have to be in the .env file, they can be defined however you see fit. If they’re accessible, they’re good to go. Otherwise nothing will run, including the dev server.

It also generates a helper getEnv function that will tell you exactly what values are there, and it will assign the proper selection (so either process.env for server, or import.meta.env for client). Pretty neat!

Feature development

Alright, the cream of the crop of this stack. Again, this was touched by me in the Feature-based development for React some time ago, but this has a bit of a nuisance here. Remember that Remix does a lot of things in the background, but in the same time, it does most of it in routes. So, how to connect a feature that does server communication with a route? Well, quite easy it turns out. Thanks to the magic of typing and exports!

Slowcore Stack comes with a exemplary “Jokes” feature which, as you might guess, fetches jokes from an API. Let’s go through it step by step.

.action file

Going alphabetically, the first is the action file.

Action is Remix is responsible for handling user input, most notably forms.

In there, we have the basic typed action with ActionFunctionArgs as a parameter and a Promise as return. In it, there’s really nothing fancy, just a bare-bones validation checking whether fields are filled and, if not, an error is being returned. If yes, perfect, we’re running this data through createJoke (which can be anything, from a simple function like here, to an API transaction, to a raw SQL query) and finally we return the response. That’s all there’s to it.

.helpers file

This is the easiest one, just a collection of small functions. If you want, you can create a helpers directory and store every function separately, but I find this pattern working well for quite some time. Obviously, if the file grows bigger, you need to take action.

.loader file

As .action is sending the data, .loader is, well, loading the data. A loader gets optional argument if you want to check the request, but in general it is responsible for fetching the data (as always, can be a raw fetch like here, can be an API client, SQL or whatever you feel like) and returning it. It can (and should!) handle errors as well!

.test file

The test is nothing fancy, just checks if everything is rendering well. I am using createRemixStub to have all the Remix-related stuff like routing and loading out of the way and simply focus on the core of the problem.

the main file

And here we are, the big boy. Main file is most often the whole React component with all the logic (outside of action and loader). In this example, it’s just a simple list rendering props and a form. But it can be expanded to whatever you want, you’re the driver here..

.types file

Simple as it gets. I like to have types declared separately, because they are often used across the whole feature, so it’s a good way to avoid any circular dependencies (like type is defined in the main file, so helper imports that, and the main file imports something from helpers) and to have a cleaner view of what’s going on.

components directory

Lastly, we’ve got a directory with some components. These can be UI or whatever else. Important thing is, you put here only things you use within this feature. If you see that something else needs this, you should move it to ui space.

./features/index.ts export

This is the main file, you shouldn’t ever use anything deeper when importing features. It exports all you might need, so an action, a loader and a component. You can obviously expose more things, but this is the important piece.

Using features in routes

Cool, so we have a feature, how to use it? If you navigate to ./routes/jokes/index.tsx, you’ll see that the entire feature is imported there. And that we have the basic loader and action async functions exported. And that’s the gist: we’re using the loader and action imported from a feature and simply return it. If we would have another feature, that would’ve been as easy as to expand the returned object, for example:

export async function loader() {
  return {
    jokes: await Jokes.loader(),
    anyOtherFearture: await AnyOtherFeature.loader(),
  };
}

Same thing with an action:

export async function action(args: ActionFunctionArgs) {
  return {
    jokes: await Jokes.action(args),
    anyOtherFearture: await AnyOtherFeature.action(args),
  };
}

Then, in the view component, we simply take the data as:

const jokesActionData = useActionData<typeof action>();
const jokesLoaderData = useLoaderData<typeof loader>();

These are prefixed with jokes, but if there would be more data fields in the returns, we’d name it differently. But the name isn’t that important. And then we’re simply rendering whatever we want, and in the place we see fit, we put the component with props got from our hooks. It’s as simple as that, and scales great too! Because your feature can be a whole page (which is kinda bad, you should split these into subfeatures), and it can be a simple alert. Again, cool thing!

Alright, that’s that for my Remix started. There are things I haven’t touched, like error handling (check ./routes/page-with-error.tsx for that), mocking (check ./mocks) and hooks, but I think these are ready for you to explore on your own!

I invite you to join me next week, where I’ll be doing handling Remix errors at scale (so handling API errors, React errors, all that stuff).

Happy coding!