Skip to content

Starter.dev and Remix: How and Why

Starter.dev helps developers get started building web apps with a variety of frameworks, showcasing how various libraries can fit together to solve similar problems. To do that, This Dot Labs has built a series of showcase apps that recreate the experience of using GitHub.

Unique among these showcases is our Remix app. Remix, unlike many of the other frameworks, is meant to run on the server. It has a server-side component unlike the other starter kits, which primarily rely on front end API calls for data fetching. In Remix, data fetching happens in loaders that run on the server, and are used to generate the page.

Remix presented several unique challenges because of its different approach to building web apps when compared to most of the other tools we showcased. Between its newness and its novel approach to JavaScript web apps, building a showcase for it wasn't as straightforward as it was for many other frameworks. This blog post details what we chose to include in our Remix GitHub clone, and how we integrated them.

Authentication

Authentication is done by creating an OAuth GitHub application. The user clicks the button to sign in with GitHub, and is then redirected to GitHub to authorize the app. After approval, they are then redirected back to the app with an access token. This token is then used to make requests to the GitHub API.

Rather than implement the OAuth flow from scratch, the Remix starter used remix-auth-github. This package made it very easy to configure authentication services with Remix. For a more general purpose version, see remix-auth.

GraphQL

We chose to use GitHub's GraphQL API to fetch our data instead of their REST API. Because we were trying to copy the GitHub interface, we knew that there would be many different components showing data from different data models all at once. This is where GraphQL shines; it let us make nested queries to fetch the data we needed with fewer calls to GitHub.

We used graphql-request to make our API calls. We chose it because it's a minimal library for making GraphQL requests, and we didn't want this showcase to be overwhelmed by the choice of a larger and more feature-rich library. Our goal was to show off Remix, not GraphQL.

GraphQL queries are made in Remix's loader functions inside route files. We use graphql-request to combine our data queries with our aforementioned auth token in order to fetch GitHub data for inclusion in our rendered page.

Tailwind

Several of the starter.dev kits also used Tailwind for styles, so we chose the same for Remix to avoid duplication of work. Remix has an approach to styling that lends itself to keeping styles separate from JavaScript, and Tailwind works well here.

Our first starter.dev kit was a NextJS app using CSS Modules to @apply Tailwind styles, co-locating those style files with the components.

Example CSS and Component from the Next app:

/* UserProfile.module.css */
@tailwind utilities;
.avatar {
  @apply rounded-full shadow z-30;
}
.name {
  @apply text-2xl text-gray-800 font-bold leading-tight;
}
.username {
  @apply text-xl text-gray-500 font-light;
}
/* ...and so on... */
// UserProfile.view.tsx, shortened for clarity
import styles from './UserProfile.module.css';
function UserProfileView() {
  return (
    <div>
      <Image
        src={avatarUrl}
        className={styles.avatar}
      />
      <h1 className="mt-2">
        <div className={styles.name}>{name}</div>
        <div className={styles.username}>{username}</div>
      </h1>
    </div>
  );
}
export default UserProfileView;

Remix didn't support CSS Modules and is generally not great at style and logic co-location. By default, Remix expects you to import a CSS file at the root of your application. Given these restrictions, the CSS Modules approach wasn't going to work in the Remix app. We wanted to do the following:

  • Keep using the Tailwind styles from the Next app
  • Leave the Tailwind classes out of the JSX to avoid clutter
  • Not have all styles located in a styles directory. But instead, co-locate them with the components

The best example of Tailwind in Remix available when we began was Kent C. Dodd's blog. But he inlines the Tailwind classes in his components, which creates the hard-to-read long lines of class names we wanted to avoid.

To accomplish our goals, we instead chose a simple approach: we kept the lists of Tailwind styles as strings exported from a Component.classNames.ts file. Since all of the styles from the Next app were exclusively Tailwind class names, we didn't have to worry about losing anything by switching from CSS Modules to just typing strings of class names.

// UserProfile.classNames.ts
export const avatar = 'rounded-full shadow z-30';
export const name = 'text-2xl text-gray-800 font-bold leading-tight';
export const username = 'text-xl text-gray-500 font-light';
// ...and so on...
// UserProfile.view.tsx, shortened for clarity
import * as styles from './UserProfile.classNames';
function UserProfileView() {
  return (
    <div>
      <img
        src={avatarUrl}
        className={styles.avatar}
      />
      <h1 className="mt-2">
        <div className={styles.name}>{name}</div>
        <div className={styles.username}>{login}</div>
      </h1>
    </div>
  );
}
export default UserProfileView;

To use these Tailwind styles, the Remix starter.dev app imports a single CSS file that is automatically generated at the application root. Concurrently is used in development to continuously run Remix and regenerate the Tailwind CSS file. This approach is very similar to the recommendations from Tailwind and Remix.

It's also possible to create a CSS file where you can use @apply in custom classes using nearly the same methods, as documented by Remix. We didn't do this because we preferred to keep style information closer to the component files rather than siloed away in a styles directory.

Storybook

Like many of the other starter.dev kits, the Remix starter uses Storybook to interactively view and build components in isolation. It also uses Vite with Storybook instead of Webpack. In testing both Webpack and Vite, we found that Vite was faster to build and reload Storybook, so we chose to use it over Webpack.

However, Storybook is not natively supported in Remix. (See here for discussion.)

This lack of support caused the greatest trouble with Remix's Link component. The Link component is used to navigate between pages in Remix, but Storybook doesn't know what to make of it. Fortunately, because we know that Link eventually renders as an a element, we were able to create a mocked version of Link just for Storybook that effectively swaps it with a. Then in the main.js configuration file, we aliased @remix-run/react to point to our mocks instead.

Architect for Deployment

Deploying Remix is a blog post unto itself. Remix includes a lot of support for pre-configured deployment types and hosts, but AWS Amplify—our target environment for the starter.dev showcases—was not initially one of them. To get it working as desired, we modified the Remix Grunge Stack and used Architect, an infrastructure-as-code tool for AWS, to deploy the Remix app.

Conclusion

When we built our Remix starter.dev showcase, the framework had not been open to the public for very long. The structure of our Remix starter reflects our best judgement of how to use Remix in those early days, and has continued to be updated as we learn more and the ecosystem matures. We welcome everyone to take a look, and contribute back to the starter if you have any improvements!