Remix Guide

react
remix
server-side rendering
full stack
web development

Installation and Project Setup

npx create-remix@latest

Choose your deployment target and preferred language (TypeScript/JavaScript).

File Structure

  • app/: Contains all your Remix code
    • entry.client.tsx: Client entry point
    • entry.server.tsx: Server entry point
    • root.tsx: Root component
    • routes/: All your route components
  • public/: Static assets
  • remix.config.js: Remix configuration

Routing

File-based Routing

  • app/routes/index.tsx/
  • app/routes/about.tsx/about
  • app/routes/blog/$slug.tsx/blog/:slug
  • app/routes/blog.tsx → Layout route for /blog/*

Nested Routes

// app/routes/dashboard.tsx
import { Outlet } from "@remix-run/react";
 
export default function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Outlet />
    </div>
  );
}
 
// app/routes/dashboard.profile.tsx
export default function Profile() {
  return <div>User Profile</div>;
}

Data Loading

Loader Function

import type { LoaderFunction } from "@remix-run/node";
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
 
export const loader: LoaderFunction = async ({ params, request }) => {
  const userId = params.id;
  const user = await getUser(userId);
  return json({ user });
};
 
export default function UserProfile() {
  const { user } = useLoaderData<typeof loader>();
  return <h1>Hello, {user.name}!</h1>;
}

Actions and Form Handling

import type { ActionFunction } from "@remix-run/node";
import { redirect } from "@remix-run/node";
import { Form, useActionData } from "@remix-run/react";
 
export const action: ActionFunction = async ({ request }) => {
  const formData = await request.formData();
  const title = formData.get("title");
  const content = formData.get("content");
  
  const errors = {};
  if (!title) errors.title = "Title is required";
  if (!content) errors.content = "Content is required";
  
  if (Object.keys(errors).length) {
    return json({ errors }, { status: 400 });
  }
  
  await createPost({ title, content });
  return redirect("/posts");
};
 
export default function NewPost() {
  const actionData = useActionData<typeof action>();
  
  return (
    <Form method="post">
      <div>
        <label htmlFor="title">Title:</label>
        <input type="text" id="title" name="title" />
        {actionData?.errors.title && <p>{actionData.errors.title}</p>}
      </div>
      <div>
        <label htmlFor="content">Content:</label>
        <textarea id="content" name="content" />
        {actionData?.errors.content && <p>{actionData.errors.content}</p>}
      </div>
      <button type="submit">Create Post</button>
    </Form>
  );
}

Error Handling

ErrorBoundary

import type { ErrorBoundaryComponent } from "@remix-run/react";
 
export const ErrorBoundary: ErrorBoundaryComponent = ({ error }) => {
  console.error(error);
  return (
    <div>
      <h1>Error</h1>
      <p>Something went wrong. Please try again later.</p>
    </div>
  );
};

CatchBoundary

import { useCatch } from "@remix-run/react";
 
export function CatchBoundary() {
  const caught = useCatch();
  
  if (caught.status === 404) {
    return <div>Page not found</div>;
  }
  
  throw new Error(`Unexpected caught response with status: ${caught.status}`);
}

Links and Navigation

import { Link, NavLink } from "@remix-run/react";
 
export default function Navigation() {
  return (
    <nav>
      <Link to="/">Home</Link>
      <NavLink
        to="/about"
        className={({ isActive }) =>
          isActive ? "active" : undefined
        }
      >
        About
      </NavLink>
    </nav>
  );
}

Meta Tags

import type { MetaFunction } from "@remix-run/node";
 
export const meta: MetaFunction = ({ data }) => {
  return [
    { title: data?.title || "My Remix App" },
    { name: "description", content: "Welcome to Remix!" },
  ];
};

CSS and Styling

CSS Modules

import styles from "~/styles/app.css";
 
export function links() {
  return [{ rel: "stylesheet", href: styles }];
}

Global Styles

// app/root.tsx
import styles from "~/styles/global.css";
 
export function links() {
  return [{ rel: "stylesheet", href: styles }];
}

Environment Variables

Access environment variables using process.env.VARIABLE_NAME

Data Mutations with useFetcher

import { useFetcher } from "@remix-run/react";
 
export default function LikeButton({ postId }) {
  const fetcher = useFetcher();
  
  return (
    <fetcher.Form method="post" action={`/posts/${postId}/like`}>
      <button type="submit">
        {fetcher.state === "submitting" ? "Liking..." : "Like"}
      </button>
    </fetcher.Form>
  );
}

Prefetching

import { Link, PrefetchPageLinks } from "@remix-run/react";
 
export default function Navigation() {
  return (
    <nav>
      <Link to="/about" prefetch="intent">About</Link>
      <PrefetchPageLinks page="/contact" />
    </nav>
  );
}

Server-Side Sessions

import { createCookieSessionStorage } from "@remix-run/node";
 
const { getSession, commitSession, destroySession } =
  createCookieSessionStorage({
    cookie: {
      name: "__session",
      secrets: ["r3m1xr0ck5"],
      sameSite: "lax",
    },
  });
 
export { getSession, commitSession, destroySession };
 
// Usage in a loader or action
export const action: ActionFunction = async ({ request }) => {
  const session = await getSession(request.headers.get("Cookie"));
  session.set("userId", user.id);
  
  return redirect("/dashboard", {
    headers: {
      "Set-Cookie": await commitSession(session),
    },
  });
};

Authentication

// app/utils/auth.server.ts
import { createCookieSessionStorage, redirect } from "@remix-run/node";
 
const sessionStorage = createCookieSessionStorage({
  cookie: {
    name: "__session",
    secrets: ["r3m1xr0ck5"],
    sameSite: "lax",
  },
});
 
export async function createUserSession(userId: string, redirectTo: string) {
  const session = await sessionStorage.getSession();
  session.set("userId", userId);
  return redirect(redirectTo, {
    headers: {
      "Set-Cookie": await sessionStorage.commitSession(session),
    },
  });
}
 
export async function getUserId(request: Request) {
  const session = await sessionStorage.getSession(
    request.headers.get("Cookie")
  );
  const userId = session.get("userId");
  if (!userId || typeof userId !== "string") return null;
  return userId;
}
 
export async function requireUserId(
  request: Request,
  redirectTo: string = new URL(request.url).pathname
) {
  const userId = await getUserId(request);
  if (!userId) {
    const searchParams = new URLSearchParams([
      ["redirectTo", redirectTo],
    ]);
    throw redirect(`/login?${searchParams}`);
  }
  return userId;
}
 
export async function logout(request: Request) {
  const session = await sessionStorage.getSession(
    request.headers.get("Cookie")
  );
  return redirect("/", {
    headers: {
      "Set-Cookie": await sessionStorage.destroySession(session),
    },
  });
}

Resource Routes

// app/routes/api/posts.tsx
import type { LoaderFunction } from "@remix-run/node";
import { json } from "@remix-run/node";
 
export const loader: LoaderFunction = async ({ request }) => {
  const posts = await getPosts();
  return json(posts);
};

Deployment

Remix supports various deployment targets:

  • Vercel
  • Netlify
  • Cloudflare Workers
  • Fly.io
  • Express Server
  • Custom Node.js server

Configure your remix.config.js accordingly:

/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  serverBuildTarget: "vercel",
  server: process.env.NODE_ENV === "development" ? undefined : "./server.js",
  ignoredRouteFiles: ["**/.*"],
  appDirectory: "app",
  assetsBuildDirectory: "public/build",
  serverBuildPath: "api/index.js",
  publicPath: "/build/",
};

Remember to always refer to the official Remix documentation for the most up-to-date information and best practices, as the framework is actively developed and may introduce new features or changes.