Today we explore remix (@remix_run), a new react framework which strives to use platform APIs as much as possible, and helps us to build resilient, performant and accessible full stack web applications.

In this blog, I will try to explain some of the features of the remix and will implement them in a to-do app.

Do not hesitate to reach out through our social media or via email to ask any questions!

Prerequisite

  • Knowlege of JavaScript and react
  • Knowledge of HTML/CSS

Development Setup

Make sure your development environment has following dependencies installed.

  • Node version greater than 14
  • npm version greater than 7

Project Setup

Create a new remix app

npx create-remix@latest

This script will ask you couple of questions, like where to create the app.

Let's say explore-remix.

The next question will be

Let's select Remix App Server for now.

Let's select TypeScript.

and press Y for installing dependencies. It will install all required dependencies, after that run the following command to change directory.

cd explore-remix

Let's run the following command to start the development server.

remix run dev

Start Building

Open app/root.tsx file, there is a lot going on in this file. But don't worry we will explore each bit. Replace the content of the file with following:

// app/root.jsx

import {
  Links,
  LiveReload,
  Outlet,
  ScrollRestoration,
} from "remix";
import type { LinksFunction } from "remix";

import globalStylesUrl from "~/styles/global.css";

export let links: LinksFunction = () => {
  return [
    { rel: "stylesheet", href: globalStylesUrl }
  ];
};

export default function App() {
  return (
    <Document>
      <Outlet />
    </Document>
  );
}

function Document({
  children,
  title
}: {
  children: React.ReactNode;
  title?: string;
}) {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width,initial-scale=1" />
        {title ? <title>{title}</title> : null}
        <Links />
      </head>
      <body>
        {children}
        <ScrollRestoration />
        {process.env.NODE_ENV === "development" && <LiveReload />}
      </body>
    </html>
  );
}

Here I would like to explain <Links/> , <Meta/> and <Outlet/> components.

It is a remix component, which allows us to add all sorts of html link tags like stylesheet links and this is the primary way of adding styles to pages in remix. To add link tags we export a links function form our route file listing all the required links as shown above. The cool things about this approach is remix will only add these links when the route is active, when the route is not active these will be removed, that way we won't encounter CSS naming conflicts. Read more here.

Outlet Component

This is primarily react-router component, which is used in parent component to render child layouts. And ofcourse remix uses react-router as its routing dependency.

The <LiveReload /> component is useful during development to auto-refresh our browser whenever we make a change. Because our build server is so fast, the reload will often happen before you even notice.

First delete the app/routes/demos folder then open app/routes/index.tsx file, this route will be the root route of your application. Replace the content of this file with the following:

// app/routes/index.tsx

export default function Index() {
  return (
    <div>
      Hello world from remix
    </div>
  );
}

Now when you visit the root route that is http://localhost:3000 you will see the following output:

Routing

Let's talk about routing in remix. This is the most important concept in remix and routes in remix corresponds to directory structure. We will be creating bunch of routes in our todo app to list all todos, to find todo by id and to create a new todo.

We will be adding following routes to our app

/todos
/todos/${todoId}
/todos/new

Let's do that! Create app/routes/todos directory and create an index.tsx file like app/routes/todos/index.tsx. Index route will be executed when the url matches /todos and add the following jsx in the index file

// app/routes/todos/index.tsx

export default function TodoIndex() {
  return (
    <div>
      <h2>My Todo's</h2>
      <ul>
        <li>Prepare breakfast</li>  
        <li>Often Prayer</li>  
      </ul>
    </div>
  )
}

Now if you go to http://localhost:3000/todos you will see the todos rendered like

As we don't have real data coming from database, let's do that real quick. For that purpose we will be using loader function from remix. Let's see it in action

// app/routes/todos/index.tsx

import type { LoaderFunction } from 'remix';
import { getAllTodos } from '~/api/todos';

type LoaderData = {
  todos: Array<{id: number, title: string}>
}

export const loader: LoaderFunction = async () => {
  const response = await getAllTodos(); 
  
  const data: LoaderData = {
    todos: response.data
  };
  return data;
}

The first line of the loader function is calling a function to get call todos, this function can be anything like RestAPI endpoint or Graphql endpoint, and then returning those todos wrapped inside an object.

That loader function will only run on server side and will never ship to client side. This shifts the whole mindset with which we used to create react applications, like data fetching on the client side using useEffect and useEffect Remix provides us a way to get that data returned from loader function to access in our react component via useLoaderData hook.

// app/routes/todos/index.tsx

import { useLoaderData } from 'remix'

...

export default function TodoIndex() {
  const { todos } = useLoaderData<LoaderData>();
  return (
    <div className="container">
      <h2>My Todo's</h2>
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <a>{todo.title}</a>
          </li>
        ))}
      </ul>
    </div>
  )
}

Now let's create second route. So create app/routes/todos/$todoId.tsx file. That's special file name to create dynamic route, in this file name todoId will become the param we will use it to fetch the corresponding todo. Let's see it in action.

// app/routes/todos/$todoId.tsx

import { useLoaderData } from 'remix;
import type { LoaderFunction } from 'remix';

import { getTodoById } from '~/api/todos';

type Todo = {
  id: number;
  title: string;
  description: string
};

type LoaderData = {
  todo: Todo
};

export const loader: LoaderFunction = async ({ params }) => {
  const response = await getTodoById(params.todoId);
  
  const data: LoaderData = {
    todo: response.data
  };
  
  return data;
}

export default function TodoRoute() {
  const { todo } = useLoaderData<LoaderData>();
  
  return (
    <div className="container">
      <p>{todo.title}</p>
      <p>{todo.description}</p>
    </div>
  );
}

Quite similar to /todos routes. Now let's add navigation from /todos to /todos/${todoId}. Edit app/routes/todos/index.tsx file to add <Link/> component from remix package.

// app/routes/todos/index.tsx

// import Link component from remix package.
import { useLoaderData, Link } from 'remix';

export default function TodoIndex() {
  const { todos } = useLoaderData<LoaderData>();
  return (
    <div className="container">
      <h2>My Todo's</h2>
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
          	{/* replace <a> with Link tag */}
            <Link to={todo.id.toString()}>{todo.title}</Link>
          </li>
        ))}
      </ul>
    </div>
  )
}

Now let's add a route to create a new todo.

Create app/routes/todos/new.tsx file and add the following content.

// app/routes/todos/new.tsx

export default function NewTodo() {
  return (
    <div>
      <form method="post">
        <h1>Add New Todo</h1>
        <div>
          <label htmlFor="title">Title</label>
          <input required type="text" id="title" name="title" />
        </div>
        <div>
          <label htmlFor="description">Description</label>
          <textarea required name="description" id="description" />
        </div>
        <button type="submit">Add Todo</button>
      </form>
    </div>
  )
}

Remix takes the classic approach for doing mutations, to do any kind of mutation(create, update, delete) on the server you can use form tag by specifying the method type. When the user submits the forms, browser serializes the form with input fields and send these fields to server which can be accessed via name attributes. Comparing this approach with what we are used to do like doing event.preventDefault on form submission and then calling the api with state managed via useState, is much simple and it works with just html no JavaScript is required.

To process this on the server we need to export action function from our route file. Like

// app/routes/todos/new.tsx

import { redirect } from 'remix'
import type { ActionFunction } from 'remix';

import { createTodo } from '~/api/todos';

export const action: ActionFunction = async ({ request }) => {
  const formData = await request.formData();
  const title = formData.get('title');
  const description = formData.get('description');
  const response = await createTodo({ title, description });

  return redirect(`/todos/${response.data.id}`);
}

request.formData() is being used to get access to request body, we are referencing to form fields via their name attribute.

I'm using a createTodo function which will call the backend API to create todo in the database and return back the response object, again we can use any technique for persistent storage like RestAPI, GraphQL etc.

Here redirect will create a response object with 302 status code and given path which will navigate to newly created todo.

Let's now add a Link component in app/routes/todos/index.tsx for navigation.

// app/routes/todos/index.tsx

export default function TodoIndex() {
  const { todos } = useLoaderData<LoaderData>();
  return (
    <div className="container">
      <h2>My Todo's</h2>
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <Link to={todo.id.toString()}>{todo.title}</Link>
          </li>
        ))}
      </ul>
      {/* Link component to navigate to new todo screen */}
      <Link to="new" className="button">Add New Todo</Link>
    </div>
  )
}

Enable JavaScript

Till now we have used react with remix to build our app, but if you go to network tab and enable JS filter you will be surprised we are downloading ZERO JavaScript in our app. So our app will continue to work if for some reason, JavaScript on the client's browser fails to load or takes too much time to load because of slow internet connection.

But we want to enable JavaScript in our site to enhance user experience and also because some of the features can't be done without JavaScript. So let's do that, it's really simple, open app/root.tsx file, import Scripts component from remix and update the Document component as follow

// app/root.tsx

function Document({
  children,
  title
}: {
  children: React.ReactNode;
  title?: string;
}) {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width,initial-scale=1" />
        {title ? <title>{title}</title> : null}
        <Links />
      </head>
      <body>
        {children}
        <ScrollRestoration />
        <Scripts />
        {process.env.NODE_ENV === "development" && <LiveReload />}
      </body>
    </html>
  );
}

With JavaScript enabled we can prefect resources, remix provides us very simple method for doing that. Let's update the app/routes/todos/index.tsx file to update the Link component to enable prefetching,  for that just add prefetch="intent"

// app/routes/todos/index.tsx

export default function TodoIndex() {
  const { todos } = useLoaderData<LoaderData>();
  return (
    <div className="container">
      <h2>My Todo's</h2>
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
          	{/* replaced with <a> tag */}
            <Link prefetch="intent" to={todo.id.toString()}>{todo.title}</Link>
          </li>
        ))}
      </ul>
      <Link to="new" className="button">Add New Todo</Link>
    </div>
  )
}

Now if the user hove over the todo link, remix will prefetch the required resources and keep them in browser cache and use it when needed. Super cool!

Libraries Support

For now remix is supporting only react, but the remix is designed in such a way that it can be used with other popular libraries like Vuejs. If we look at the code repository of the remix, we can see that