Skip to content

Remix Run By Example

Posted on:May 12, 2021

So this post is a bit similar to another one I did which is called Builing a Todo List with Supabase and Nextjs. I will basically be rebuilding this in a new paid js framework called Remix Run.

If you need a refresher of what we built last time look below.

Getting started

In order to get started with Remix you will need to run npm init remix and follow the prompts including putting in your license key. Yes this is paid software which is maybe a bit unusual but I think its worth it.

Setting up our env variables

Now this is something that works out of the box in nextjs but you have to do a bit of manual setup with remix.

npm install dotenv
touch .env

In your .env file have the following with your keys filled in from Supabase.

SUPABASE_URL=
SUPABASE_ANON_KEY=

The next bit of configuration is to add the dotenv loader toward the top of app/entry.server.jsx

import ReactDOMServer from "react-dom/server";
import { RemixServer } from "remix";

const dotenv = require("dotenv");
dotenv.config();

Let’s also add the supabase client js library:

npm install @supabase/supabase-js

Now restart remix server which will be npm run dev

Then in an app/utils directory add a file called initSupabase.js with the following contents.

import { createClient } from "@supabase/supabase-js";

const supabase = createClient(
  process.env.SUPABASE_URL,
  process.env.SUPABASE_ANON_KEY
);

export default supabase;

Reading Todos

Head on over to app/routes/index.jsx where we will do the more interesting coding.

Remix has this concept of a loader which is similar to getServerSideProps in nextjs. To load our todos its this simple:

import { json, useRouteData } from "remix";
import supabase from "../utils/initSupabase";

export async function loader() {
  const { data } = await supabase.from("todos").select("id, title, completed");

  return json({
    allTodos: data,
  });
}

Then in our component we can useRouteData and then map over our todos.

import { json, useRouteData } from "remix";

export default function Index() {
  const { allTodos } = useRouteData();
  return (
    <ul>
    {allTodos.map((todo) => (
        <li key={todo.id}>
        {todo.completed ? <strike>{todo.title}</strike> : todo.title}
        </li>
    ))}
    </ul>
  )
}

Data mutations (Create, Update, Delete)

I think this is the thing I find the coolest, but it is also a throwback to how we used to do data mutations before React.

Let’s create a Remix Form (uppercased) which is basically just a supercharged <form>

import { Form, json, useRouteData } from "remix";

<Form replace method="post">
    <input type="text" name="newTodo" />
    <button type="submit">Add New Todo</button>
</Form>

Now this is a bit of a mental model shift. You might wonder where the onChange, and onClick is that you normally have to do with React. Well in fact forms can package this data up without individual state fields per form input. The cool thing is we could have 10 fields and they will all get serialized super easily.

It will basically do an ajax post request to this same route with all your data in it.

We can use it with an action method as follows:

import { Form, json, useRouteData, redirect } from "remix";

export async function action({ request }) {
  let body = new URLSearchParams(await request.text());

  if (request.method.toLowerCase() === "post") {
    await supabase.from("todos").insert([{ title: body.get("newTodo") }]);
  }

  return redirect("/");
}

To access the field value from the post request we use body.get("newTodo")

Now you can fill out the delete and complete functionality using your old school form knowledge.

<Form replace method="put">
    <input type="hidden" name="completed" value={!todo.completed} />
    <input type="hidden" name="id" value={todo.id} />
    <button type="submit" className="complete">
        Complete
    </button>
</Form>
<Form replace method="delete">
    <input type="hidden" name="id" value={todo.id} />
    <button type="submit">Delete</button>
</Form>

and then change your action to the following:

export async function action({ request }) {
  let body = new URLSearchParams(await request.text());

  if (request.method.toLowerCase() === "post") {
    await supabase.from("todos").insert([{ title: body.get("newTodo") }]);
  } else if (request.method.toLowerCase() === "put") {
    await supabase
      .from("todos")
      .update({ completed: body.get("completed") })
      .match({ id: body.get("id") });
  } else if (request.method.toLowerCase() === "delete") {
    await supabase
      .from("todos")
      .delete()
      .match({ id: body.get("id") });
  }

  return redirect("/");
}

Taking a look back at the code I had to write, this does seem a bit simpler than Nextjs.