Skip to content

Instantly share code, notes, and snippets.

@budiantoip
Last active February 1, 2025 04:55
Show Gist options
  • Save budiantoip/6d911a68a8b53a3c12a4dcfcbf3b0b56 to your computer and use it in GitHub Desktop.
Save budiantoip/6d911a68a8b53a3c12a4dcfcbf3b0b56 to your computer and use it in GitHub Desktop.
Remix Cheatsheet

References

Introduction

Remix is built on top of React Router. It uses JSX.

Remix Entrypoint File

app/root.tsx

Adding Stylesheets

Put this in the root.tsx file:

import type { LinksFunction } from "@remix-run/node";
// existing imports

import appStylesHref from "./app.css?url";

export const links: LinksFunction = () => [
  { rel: "stylesheet", href: appStylesHref },
];

Remix Routes

The routes are defined by creating a file in app/routes folder. For example:

touch app/routes/contacts.\$contactId.tsx

The . will create a / in the URL, so the above command will handle the /contacts/:contactId route.

Render Child Routes

To render child routes inside of parent layouts, we need to render an Outlet component in the root.tsx file.

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

export default function App() {
  return (
    <html lang="en">
      {/* other elements */}
      <body>
        <div id="sidebar">{/* other elements */}</div>
        <div id="detail">
          <Outlet />
        </div>
        {/* other elements */}
      </body>
    </html>
  );
}

Client Side Routing

Client side routing allows our app to update the URL without requesting another document from the server. Instead, the app can immediately render child components, with <Link>.

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

export default function App() {
  return (
    <html lang="en">
      {/* other elements */}
      <body>
        <div id="sidebar">
          {/* other elements */}
          <nav>
            <ul>
              <li>
                <Link to={`/contacts/1`}>Your Name</Link>
              </li>
              <li>
                <Link to={`/contacts/2`}>Your Friend</Link>
              </li>
            </ul>
          </nav>
        </div>
        {/* other elements */}
      </body>
    </html>
  );
}

Loading Data

There are two APIs we'll be using to load data, loader and useLoaderData. First we'll create and export a loader function in the root.tsx file and then render the data.

import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { getContacts } from "./data";

export const loader = async () => {
  const contacts = await getContacts();
  return json({ contacts });
};

export default function App() {
  const { contacts } = useLoaderData();

  return (
    <html lang="en">
      {/* other elements */}
      <body>
        <div id="sidebar">
          {/* other elements */}
          <nav>
            {contacts.length ? (
              <ul>
                {contacts.map((contact) => (
                  <li key={contact.id}>
                    <Link to={`contacts/${contact.id}`}>
                      {contact.first || contact.last ? (
                        <>
                          {contact.first} {contact.last}
                        </>
                      ) : (
                        <i>No Name</i>
                      )}{" "}
                      {contact.favorite ? (
                        <span></span>
                      ) : null}
                    </Link>
                  </li>
                ))}
              </ul>
            ) : (
              <p>
                <i>No contacts</i>
              </p>
            )}
          </nav>
        </div>
        {/* other elements */}
      </body>
    </html>
  );
}

Type Inference

Type inference is an automatic data type detection. We can achieve this by adding a typeof.

// existing imports & exports

export default function App() {
  const { contacts } = useLoaderData<typeof loader>();

  // existing code
}

URL Params in Loaders

To utilize URL params, we can use params.<param_name>. For example: params.contactId.

import { json } from "@remix-run/node";
import { Form, useLoaderData } from "@remix-run/react";
// existing imports

import { getContact } from "../data";

export const loader = async ({ params }) => {
  const contact = await getContact(params.contactId);
  return json({ contact });
};

export default function Contact() {
  const { contact } = useLoaderData<typeof loader>();

  // existing code
}

// existing code

Validating Params and Throwing Responses

import type { LoaderFunctionArgs } from "@remix-run/node";
// existing imports
import invariant from "tiny-invariant";

// existing imports

export const loader = async ({
  params,
}: LoaderFunctionArgs) => {
  invariant(params.contactId, "Missing contactId param");
  const contact = await getContact(params.contactId);
  return json({ contact });
};

// existing code

Note that the invariant function will check the input parameter, and throw an error message if it's not supplied.

How to Handle Empty Data

We can throw an error if we receive empty result when retrieving from database.

// existing imports

export const loader = async ({
  params,
}: LoaderFunctionArgs) => {
  invariant(params.contactId, "Missing contactId param");
  const contact = await getContact(params.contactId);
  // The following lines will throw an error
  if (!contact) {
    throw new Response("Not Found", { status: 404 });
  }
  return json({ contact });
};

// existing code

How to Handle Form Requests

Let's say we have this HTML elements:

export default function Contact() {
  const { contact } = useLoaderData<typeof loader>();
  return (
    <div id="contact">
    </div>
    <div>
      <div>
          <Form action="edit">
            <button type="submit">Edit</button>
          </Form>

        </div>
    </div
  )
}

Basically, the form will send a request to /contacts/:contactId/edit, and we can handle the backend by creating this file:

touch app/routes/contacts.\$contactId_.edit.tsx

And we put this in the file:

import type { LoaderFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import { Form, useLoaderData } from "@remix-run/react";
import invariant from "tiny-invariant";

import { getContact } from "../data";

export const loader = async ({
  params,
}: LoaderFunctionArgs) => {
  invariant(params.contactId, "Missing contactId param");
  const contact = await getContact(params.contactId);
  if (!contact) {
    throw new Response("Not Found", { status: 404 });
  }
  return json({ contact });
};

export default function EditContact() {
  const { contact } = useLoaderData<typeof loader>();

  return (
    <Form key={contact.id} id="contact-form" method="post">
      <p>
        <span>Name</span>
        <input
          defaultValue={contact.first}
          aria-label="First name"
          name="first"
          type="text"
          placeholder="First"
        />
        <input
          aria-label="Last name"
          defaultValue={contact.last}
          name="last"
          placeholder="Last"
          type="text"
        />
      </p>
      <label>
        <span>Twitter</span>
        <input
          defaultValue={contact.twitter}
          name="twitter"
          placeholder="@jack"
          type="text"
        />
      </label>
      <label>
        <span>Avatar URL</span>
        <input
          aria-label="Avatar URL"
          defaultValue={contact.avatar}
          name="avatar"
          placeholder="https://example.com/avatar.jpg"
          type="text"
        />
      </label>
      <label>
        <span>Notes</span>
        <textarea
          defaultValue={contact.notes}
          name="notes"
          rows={6}
        />
      </label>
      <p>
        <button type="submit">Save</button>
        <button type="button">Cancel</button>
      </p>
    </Form>
  );
}

Index Route

Index route is a default child component the app will render when the page is loaded for the first time.

touch app/routes/_index.tsx
export default function Index() {
  return (
    <p id="index-page">
      This is a demo for Remix.
      <br />
      Check out{" "}
      <a href="https://remix.run">the docs at remix.run</a>.
    </p>
  );
}

Cancel Button

We'll need a click handler on the cancel button by utilizing useNavigate.

// existing imports
import {
  Form,
  useLoaderData,
  useNavigate,
} from "@remix-run/react";
// existing imports & exports

export default function EditContact() {
  const { contact } = useLoaderData<typeof loader>();
  const navigate = useNavigate();

  return (
    <Form key={contact.id} id="contact-form" method="post">
      {/* existing elements */}
      <p>
        <button type="submit">Save</button>
        <button onClick={() => navigate(-1)} type="button">
          Cancel
        </button>
      </p>
    </Form>
  );
}

URLSearchParams and GET Submissions

Given this URL:

http://localhost:5173/?q=ryan

We can get the q value by:

import type {
  LinksFunction,
  LoaderFunctionArgs,
} from "@remix-run/node";

// existing imports & exports

export const loader = async ({
  request,
}: LoaderFunctionArgs) => {
  const url = new URL(request.url);
  const q = url.searchParams.get("q");
  const contacts = await getContacts(q);
  return json({ contacts });
};

// existing code
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment