Fullstack serverless web development with Remix and Architect (Part 3, UI code separation)

Tuomas Koivistoinen

In part 2 of our series on Remix and Architect, we refactored our app to use nested routing. In this part we will refactor some of the UI code into their own components for a more consistent UX, better code reusability and readability.

UI code separation

Let’s make a directory app/components and put all the UI components there. I think we can just copy code there route by route, remove unnecessary bits and parameterise the bits where we want variance.

Notes route

Let’s call this component MainArea.tsx. We first copy the notes.tsx code and leave only the layout bits, parameterising the parts where we want variance.

import { Link, NavLink, Outlet } from "@remix-run/react";

interface MainAreaProps {
  items: Array<{ id: string; title: string }>;
  labelAdd: string;
  labelEmptyState: string;
}

export default function MainArea({
  items,
  labelAdd,
  labelEmptyState,
}: MainAreaProps) {
  return (
    <main className="flex h-full bg-white">
      <div className="h-full w-80 border-r bg-gray-50">
        <Link to="new" className="block p-4 text-xl text-blue-500">
          {labelAdd}
        </Link>

        <hr />

        {items.length === 0 ? (
          <p className="p-4">{labelEmptyState}</p>
        ) : (
          <ol>
            {items.map((item) => (
              <li key={item.id}>
                <NavLink
                  className={({ isActive }) =>
                    `block border-b p-4 text-xl ${isActive ? "bg-white" : ""}`
                  }
                  to={item.id}
                >
                  📝 {item.title}
                </NavLink>
              </li>
            ))}
          </ol>
        )}
      </div>

      <div className="flex-1 p-6">
        <Outlet />
      </div>
    </main>
  );
}

Now we can refactor our new notes page to use this component.

import type { LoaderFunction } from "@remix-run/node";
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";

import { requireUserId } from "~/session.server";
import { getNoteListItems } from "~/models/note.server";
import MainArea from "~/components/MainArea";

type LoaderData = {
  noteListItems: Awaited<ReturnType<typeof getNoteListItems>>;
};

export const loader: LoaderFunction = async ({ request }) => {
  const userId = await requireUserId(request);
  const noteListItems = await getNoteListItems({ userId });
  return json<LoaderData>({ noteListItems });
};

export default function NotesPage() {
  const data = useLoaderData() as LoaderData;

  return (
    <MainArea
      items={data.noteListItems}
      labelAdd={"+ Create Note"}
      labelEmptyState={"No notes yet"}
    />
  );
}

App route

Let’s do the same for the app route. We are not going to be reusing it, but it makes the route files a bit cleaner. We’ll call this component Navbar. Now let’s just do the same thing we did to the notes route.

import { Form, Link, Outlet } from "@remix-run/react";

interface NavbarProps {
  links: Array<{ to: string; label: string }>;
  title: string;
}

export default function Navbar({ links, title }: NavbarProps) {
  return (
    <div className="flex h-full min-h-screen flex-col">
      <header className="flex items-center justify-between bg-slate-800 p-4 text-white">
        <h1 className="text-3xl font-bold">
          {links.map(({ to, label }, idx) => (
            <Link className={idx === 0 ? undefined : "ml-8"} key={to} to={to}>
              {label}
            </Link>
          ))}
        </h1>
        <p>{title}</p>
        <Form action="/logout" method="post">
          <button
            type="submit"
            className="rounded bg-slate-600 py-2 px-4 text-blue-100 hover:bg-blue-500 active:bg-blue-600"
          >
            Logout
          </button>
        </Form>
      </header>

      <Outlet />
    </div>
  );
}

Now we can use it in app/routes/app.tsx.

import Navbar from "~/components/Navbar";
import { useUser } from "~/utils";

export default function AppPage() {
  const user = useUser();
  return (
    <Navbar
      links={[{ to: "/app/notes", label: "Notes" }]}
      title={user.email}
    />
  );
}

UI sandboxes

Note that, if we were using a UI sandbox such as React Cosmos, this type of UI code separation would be ideal for UI sandbox development. I think this is an ideal approach for larger projects, especially when you are using a Design System or when you have a dedicated frontend UI developer.

Coming Up in Part 4

In this part, we separated some of the UI code from routes to make a more consistent UX, have more reusable code and increase readability. When you are ready, please join me in the next part where we will do the same for forms but go the extra mile by adding a few modules to make them even more succinct.

Get in Touch.

Let’s discuss how we can help with your cloud journey. Our experts are standing by to talk about your migration, modernisation, development and skills challenges.

Ilja Summala