A Beginners Guide to Remix Framework

Although, we wouldn't exactly call Remix a "new" framework, since it was previously available as a subscription-based premium framework and it was just launched as an open-source framework under an MIT license. So, hey again, Remix, nice to see your new getup, let's see what's under the hood.

What is Remix?

Remix is an edge-first server-side rendering JavaScript framework built on React that allows us to build full-stack web applications thanks to its frontend and server-side capabilities. On the frontend, it acts as a higher level React framework that offers server-side rendering, file-based routing, nested routes and a lot more, while on the backend it can be used to run a server of its own. The purpose of this article is to explore Remix as a beginner from the ground up to see how it works, and how it differs from your typical Javascript framework.

Prerequisites

  • Basic understanding of React.
  • Node.js v14 or greater installed
  • NPM v7 or greater installed
  • A code editor (I’ll be using VSCode)

Scaffolding a New Remix App

If you’re familiar with building React apps (which I assume you are), then you are no stranger to the “create-react-app” npx command used to quickly spin up a new react development environment with a basic boilerplate demo app created for us. It’s no different with Remix. To create a new Remix app, all we have to do is start by running the below command in our terminal.

npx create-remix@latest

After that, you are presented with an interactive welcome screen, prompting you to determine what should be included in the boilerplate demo app.

Where would you like to create your app?

If you wish to have “my-remix-app” directory created for you, simply hit enter or manually provide the name of choice. Or better yet, if you’re currently in the folder you wish to use, simply hit period “.” to signify that you want it created in the current directory.

Where do you want to deploy?

Next, you’re asked for your deployment choice with a list of options. We'll be using the Remix App Server as it is the recommended choice.

Typescript or JavaScript?

To keep things straightforward, we’ll also be using vanilla JavaScript.

Finally, you're asked if you wish to run npm install, which obviously is a yes - to automatically have all the required dependencies installed for us. After the installation is complete, we are presented with a scaffolded remix demo app.

A Detailed Look at Remix File Structure and Cleanup

Once everything installs, we’re presented with a bunch of files and folders, as you can see in the image below.

  • app - This is pretty much the only folder we need to worry about as it is the one that holds our application and where most of our app development will take place. We’ll be exploring it further in a minute.
  • node_modules - This is the default node modules dependencies folder.
  • public - This is the public folder that holds the static files and assets that are served to the browser when our app is built or deployed.
  • .gitignore - As the name suggests, this is where we specify what files should be ignored when deployed.
  • jsconfig.json - Is a compiler config file that is of no importance to us right now.
  • package-lock.json & package.json - These are needed for tracking the versions of our dependencies tree used in our application.
  • remix.config.js - This is a required configuration file for our remix app that lets remix know how to build our app for production and how to run it in development, again, not something we should concern ourselves with at this time.

With that out of the way, let's get our application running with a run dev command.

npm run dev

This should kickstart our demo app and have it running on localhost, port 3000. Upon visiting this address, we should have our demo app looking like the below screenshot and you could take a look around to get a feel for it.

With our app up and running, let’s dive back into the app directory to see what exactly we have in there:

  • routes - The routes folder is, as the name says, where we define the various routes for the pages in our application. Opening it up you’ll find a bunch of routes already created for the demo app. We'll go more into routes later on in the article.
  • styles - This folder consists of all the styles used in the demo application.
  • entry.client.jsx - This is our remix app's very entry point when being loaded on the client. It is through this file that the static HTML that was rendered on the server is hydrated and injected into the DOM. It is worth noting that this file only runs on the client side.
  • entry.server.jsx - This is our remix app’s entry point to the server. It is where the markup of our application is generated on the server and provides full control over the server response. As you might have guessed, these entry files are important as they provide us with full control over our app's entry points and it is also worth noting that this file runs only on the server.
  • root - This is the default root component of our application. If this was a basic React app, this would be the default App.jsx file in the src folder of our React application. It is the initial root component that acts as the index page of our app, where we start developing our application in and what gets initially rendered to the DOM when we run our application.

Clean Up

To avoid getting overwhelmed with the demo app's files and code, let's delete the routes and styles folder from the app directory. Then, we'll proceed with deleting the actual code present in the root.jsx file, which will cause the webpage to be blank.

Once we have a blank page to work with, let's create a simple hello world in the root component.

export default function App() {
  return <h1>Hello World</h1>
}

The resulting output would be:

Something to consider is that if we made any changes to this webpage, like modifying the text, we would not see the changes reflected unless we manually refresh the page. This is because remix doesn’t offer live reload right out of the box.

When changes are made to our component in development, we have to manually refresh the webpage to get the reflected changes in our output. To get around this, remix provides a LiveReload component that can be imported from remix and used in our app to enable live reload. This component is going to detect any changes in our remix application and reloads the page whenever there is a change detected.

import { LiveReload } from 'remix'

export default function App() {
  return (
    <>
      <h1>Hello World</h1>
      <LiveReload />
    </>
  )
}

That's it! Now whatever changes we make to this component will get automatically updated in the DOM.

Another thing we should know is that when we inspect the source code for this web page with the browser's dev tool, we should notice that what was sent to the client is just a plain web page with nothing but the returned h1 text element.

As you can see, there are no included meta tags or title. This is because remix gives us full control over our page. You have to manually return a fully constructed HTML template, including the head, meta tags and body that should be rendered in the DOM.

When refactoring our code, a typical remix root component would look something like:

import { LiveReload } from 'remix'

export default function App() {
  return (
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <title>My Remix App</title>
      </head>
      <body>
        <h1>Hello World</h1>

        <LiveReload />
      </body>
    </html>
  )
}

This would yield the same result as earlier but now with the appropriate meta info and title attached in the source file.

How to Work With Routes in Remix?

If you’ve ever created a React application with multiple pages or routes, you're probably used to setting up these routes yourself using something like react-router – but Remix uses a file-based routing system for its routes.

What this means is if you wanted to create a page route, you’d simply need to create an explicit directory called "routes" inside of the app directory. Whatever file that is then created inside of this routes directory automatically gets treated as a route.

So let's quickly create a routes directory and inside this folder, a single route called posts.jsx

Inside this route component, we’ll return a basic header text.

export default function posts() {
  return (
    <div>
      <h1>This is a route for posts</h1>
    </div>
  )
}

Now that we have a basic posts route set up, you’d naturally assume that by simply visiting the URL for this route localhost/3000/posts ,` we’d land on this page, right?

That is not the case, if anything, this is where remix gets somewhat tricky and confusing.

Why? Because remix uses the root.jsx file as its base index file for rendering all created routes or pages by nesting them within the root.jsx file.

To get this to work, we are provided with another remix component called <Outlet/> that can be imported from remix and used to render our routes inside the root.jsx file. This Outlet component is responsible for figuring out what route should be loaded when we hit a specific URL. So, let’s refactor our root.jsx file to utilize the Outlet component:

import { LiveReload, Outlet } from 'remix'

export default function App() {
  return (
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <title>My Remix App</title>
      </head>
      <body>
        <h1>Hello World</h1>
        <Outlet />
        <LiveReload />
      </body>
    </html>
  )
}

Now, if we visited just the home URL, we’d land at the expected home page with just the “Hello World” text, but if we visited the newly created route /posts , the Outlet component would immediately render that route within the root component as a child nested route – see image:

A bit confusing? Let’s have another look. To create a route, it has to be created inside of the routes folder within the app directory.

For instance, if we create an index.jsx file inside of the routes folder, it would automatically get loaded once we visited the homepage of our application, but to get it to display, we’d have to use the Outlet component provided by remix within the root.jsx file as this is the base entry file used by remix to render the currently active route. But it doesn’t end there, we could nest other routes within other routes.

Let's say we create a new folder within the routes directory called posts and within this posts folder we create another file called new.jsx (which technically means we have two distinct routes sharing the same name i.e posts.jsx and the posts directory we just created):

Now because this newly created directory bears the same name as an already existing route posts.jsx, remix will try to nest whatever file is found in the newly created posts directory within the post.jsx file as a nested child route.

Meaning that:

  • If we visited localhost:3000/posts, we’d be served the posts.jsx file as it is,
  • but if we tried accessing the localhost:3000/posts/new route, remix would try to nest the new.jsx route within the posts.jsx route,

But for this to work, we’d have to use the <Outlet/> component within the posts.jsx file like below:

import { Outlet } from 'remix'

export default function Posts() {
  return (
    <div>
      <h1>This is a parent route for posts</h1>
      <Outlet />
    </div>
  )
}

Now, whatever route created inside the posts folder will be loaded inside this post.jsx component as a child route, using the Outlet component as a means of rendering the nested child route (i.e the new.jsx file would be nested in here).

How do Dynamic Routes Work in Remix?

Oftentimes, we want to create a dynamic route on the go based on a user's request. As an example, let's say we have multiple blog posts in a database that we wish to dynamically load based on the exact post requested. For such a scenario, remix supports dynamic routing and to create a dynamic route, the naming convention of the file responsible for this dynamic route would have to start with a dollar sign, followed by the name of the route e.g $postid.jsx .

So let’s do just that. We are going to create a dynamic route within our posts directory and name this file $postid.jsx .

Now whenever we attempt to visit any random route that is not explicitly defined within the posts route, remix will automatically render this dynamic route component for it. Tricky, huh? This is what would happen if we tried visiting any of the below routes.

  • localhost/3000/posts - remix would load up post.jsx for this route as the file exist.
  • localhost/3000/posts/new - remix would load up new.jsx component for this route as it exist.
  • localhost/3000/posts/random123 - remix would check if such a route is explicitly defined in the posts directory, when it fails to find one, it will then check for any dynamic route, which would lead it to load up the $postid.jsx.

But, we might want to know what exact URL was visited by a user in the dynamic route, and then probably do something with this information, like make a request to a database for the exact post that was requested.

For this scenario, we have the useParams hook that can be used to get access to the current URL parameter loaded by the dynamic route. The exact info for this is stored as a property of the returned params object using the exact name we gave to the dynamic route.

import { useParams } from 'remix'

export default function Post() {
  const params = useParams()

  return (
    <div>
      <h2>You are currently accessing {params.postid} post</h2>
    </div>
  )
}

Output:

Now that we have a basic understanding of how routes work in remix, let’s move on.

Remix SEO with Meta Tags

Remix lets you dynamically set the SEO meta information and social links for each route module by providing us with a Meta component that can be placed inside the head of our HTML document.

This Meta component is responsible for injecting the desired meta tags into the page. To get it to do that, we first have to define an export meta function that simply returns an object containing a key-value pair of the desired meta-information.

When we load up a route component, this Meta checks the route module for any exported meta function which is then used to inject the provided information in the head of our HTML document for that page and automatically removes it when we leave the page.

import { Meta } from 'remix'

export const meta = () => {
  return {
    title: 'A title for this route',
    description: 'A description for the route',
    keywords: 'remix, javascript, react'
  }
}

export default function App() {
  return (
    <html lang="en">
      <head>
        <Meta />
      </head>
      <body>
        <h1>Testing SEO Tags</h1>
      </body>
    </html>
  )
}

As you see, the head of this HTML document is empty. We used the <Meta/> component in there which checks for an exported meta function and injects the returned information into the head. If we ran this and viewed the source code that is exactly what we would see:

Working with Styles in Remix

When it comes to styling, Remix uses the traditional linking to an actual stylesheet to style a route, and just like the SEO meta tags, we can dynamically set a stylesheet for each route using a <Links/> component provided by Remix.

We can use that <Links/> component to inject whatever stylesheet which needs to be loaded for a specific route module when we visit it. Just like the meta tags, we’d have to first define an exported links function that returns an array containing an object for each link that we wish to inject into the route, these links are then automatically removed when we leave that route.

To create a stylesheet, we start by first creating a styles directory in our app. Then, we can create a global.css file for our app or manually create a separate stylesheet for each route.

To use this stylesheet, check the code below:

import { Links } from 'remix'
import globalStyleURL from '~/styles/global.css'

export const links = () => {
  return [{ rel: 'stylesheet', href: globalStyleURL }]
}

export default function App() {
  return (
    <html lang="en">
      <head>
        <title>Just a title</title>
        <Link />
      </head>
      <body>
        <h1>Testing Styling</h1>
      </body>
    </html>
  )
}

If we viewed the source code for this, we’d find that the stylesheet has been added to our application as a link tag.

Data Loading

Remix introduces a new concept for performing side effects such as fetching or loading data from a database or talking to an external API.

This is achievable using a new asynchronous function called loader that runs on the server and is solely responsible for prefetching data before the component is rendered on the server. The loader function is accompanied by a new hook called useLoaderData that can be utilized inside of our component to get access to the loaded data that is returned by the loader function after the data has been fetched.

To use a loader function in a component, we start by first importing the useLoaderData hook from remix and then defining a loader function as an export.

import { useLoaderData } from 'remix'

export const loader = async () => {
  // make a request to an api or get data from database
  return { title: 'A random post title' }
}

export default function App() {
  const data = useLoaderData()

  return <h1>{data.title}</h1>
}

After setting up a loader, we can access whatever data is returned by this loader using the useLoaderData hook in our component.

Working with Forms in Remix

When it comes to forms, Remix does away with the need to manually hook up forms to state or handle form submission on the client-side using an onSubmit event listener like in a typical React application. Instead, Remix provides us with an action function that automatically gets access to the form data in our form when submitted and then uses native “post” and “get” request methods to send and mutate data in the form. This is similar to how forms are handled in traditional languages like PHP.

Whenever there is a form submission, this action function is triggered to handle the submission, with the request object containing the form data passed to the action handler function by default. This action function then runs on the server where we can easily talk to a database with the form data - gone is the era of client-side mutations.

To create a form, you can either use the traditional HTML form element <form> or import a Form component from remix that behaves the same as the traditional form element, but uses a fetch API to send data from the form and is believed to be faster. Whatever form data is inputted inside the form fields are then sent to the action function on the server as a request object that can then be accessed within the action function using the names of the input fields.

Let’s create a basic form using our new.jsx route component in the posts directory.

import { Form, redirect } from 'remix'

export const action = async ({ request }) => {
  const form = await request.formData()
  const title = form.get('title')
  const content = form.get('content')

  console.log({ title, content })
  return redirect('/')
}

export default function NewPost() {
  return (
    <div>
      <h1>Add a new post</h1>

      <Form method="POST">
        <label htmlFor="title">
          Title: <input type="text" name="title" />
        </label>

        <label htmlFor="content">
          Content: <textarea name="content" />
        </label>

        <input type="submit" value="Add New" />
      </Form>
    </div>
  )
}

Did you see we also imported a redirect function from Remix? This redirect is basically the same as the redirect found in react-router.

We used it to specify that once the form gets submitted, remix should redirect the user to the index route which is the homepage. This is typically when we’d want to use the submitted form data to mutate a database, but to keep things simple enough to understand, we’re just going to console log whatever is entered into the input fields to the servers console because the action function runs only on the server. So let’s do just that:

Output:

It is worth noting that any form submission that uses the “post” method is automatically handled by the action function that is provided in the component, but when the submission uses a “get” method, remix requires that you have a loader function defined that handles the form data on the server.

Handling Errors in Remix

With most frameworks, when there is an error in a component or a route fails to load, it will result in the entire page being broken and an error screen being displayed with the error message.

But with errors in Remix, when we create a component or route, we can equally define a root boundary error template that potentially catches any error that occurs in that component or route and when this error occurs. The error boundary template will be rendered in place of the actual component or route, and this error will only affect that component or route that has an error or failed to render by showing this custom error template.

export default function App() {
  //...component fails to load or throws an error
}

export function ErrorBoundary({ error }) {
  console.error(error)
  return (
    <html>
      <head>
        <title>There was an Error</title>
      </head>
      <body>{/* The error UI you want your users to see */}</body>
    </html>
  )
}

The good thing about remix error boundaries is that we do not have to set up error boundaries in every component or route because when an error occurs in a nested component or route that doesn’t have an error boundary template, the error bounces up the component/route tree to the nearest parent with an error boundary.

So, you don't have to add error boundaries to every route, only when you want to add that extra UI touch to your component/route.

[@portabletext/react] Unknown block type "message", specify a component for it in the `components.types` prop

Refactoring Our App

Remember earlier I mentioned that when creating a remix page, we have to return a fully composed HTML template, with both the head and body included? And then we went ahead to do just that in our root.jsx index file?

Also, that this file is used by remix as the actual index file of our application that loads up whatever current active route?

When we build an actual app, we would like to have the same basic layout that wraps around our entire app regardless of what route we are on. This layout could simply consist of a standard navbar and footer and then dynamically load different title, meta details and content for each new page route.

For this purpose, we’ll refactor our root.jsx to make it leaner and then go through the code below:

import { LiveReload, Link } from 'remix'

export default function App() {
  return (
    <Document title="My Remix App">
      <Layout>
        <Outlet />
      </Layout>
    </Document>
  )
}

// Put the html skeleton in a component to make it easier to wrap every route
export function Document({ children, title }) {
  return (
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        {title ? <title>{title}</title> : null}
      </head>
      <body>
        {children}

        {/*Enable live reload in development environment only, not production */}
        {process.env.NODE_ENV === 'development' ? <LiveReload /> : null}
      </body>
    </html>
  )
}

// A standard layout that will be the same for each route
export function Layout({ children }) {
  return (
    <>
      <nav className="navbar">
        <Link to="/">Remix App</Link>
        <ul>
          <li>
            <Link to="posts">Posts</Link>
          </li>
          <li>
            <Link to="posts/new">New Post</Link>
          </li>
        </ul>
      </nav>
      {children}
    </>
  )
}

export function ErrorBoundary({ error }) {
  console.log(error)
  return (
    <Document>
      <Layout>
        <h1>There was an Error</h1>
        <p>{error.message}</p>
      </Layout>
    </Document>
  )
}

We started by creating a Layout component that will be the base layout for wrapping our entire app. This is possible using the children prop just like in a typical React app, this children prop is responsible for taking in whatever content that the Layout component wraps around.

The Layout component also has a navbar with a few links (Note: Remix uses the same identical Link component found in React Router for its navigation).

Next, instead of having the HTML document skeleton directly in our root app component as we initially did, we created a Document component with children prop to wrap around the Layout, giving it a fully composed HTML template. We also provided a title prop that will be responsible for dynamically specifying the title of the currently active route.

Next, we recomposed the root App component, wrapping the Document around the Layout of our app, and then wrapping the Layout component around the Outlet component that loads up whatever active route we are in.

Lastly, we set up an error boundary template to catch any potential error that might occur in this component or any of the nested routes. That’s all. If by any chance you took a peek at the initial code in the demo app before we cleaned it up, this is the exact setup you’d have seen and It would all make sense now as we have pretty much covered everything that was in there.

Difference Between Remix and Next.js

Both Remix and Next.js support SSR, so there doesn't appear to be much of a difference between them at first glance, the only major difference between them is that Next.js supports SSG while Remix only focuses on SSR.

But Remix takes an interesting and somewhat unorthodox approach in solving most problems. From its nested routes, loaders and form actions concept, to how errors are handled, Remix introduces a whole new concept to developing full-stack SSR applications that makes it unique and quite different from Nextjs.

Conclusion

Although Remix is still new, it has a small but very active community thriving around it. And as a general overview, it comes with some pretty cool built-in features that make it a unique and strong contender for already existing solutions like Next.js. I think that with time and support from the open-source community, it can go on to be an amazing framework for building amazing SSR applications.

Some folks feel Remix is against Jamstack, SSGs and traditional SSR frameworks, but that doesn’t have to be the case. As told by Ryan Florence, one of Remix's co-founders, “The people building these technologies are all friends - It’s people talking about it online that make it seem like there is rivalry." (source).

With that said, we can say Remix is here to coexist with these technologies and does have some use cases where it shines the most. Sites with lots of dynamic content would benefit from Remix as it is Ideal for applications involving databases, dynamic data, user accounts with private data, etc.

There is much more to Remix than simply a React framework and we could go on talking about the many more features Remix has to offer as we have only scratched the surface – but we hope to learn and share more in the future.