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 codeentry.client.tsx
: Client entry pointentry.server.tsx
: Server entry pointroot.tsx
: Root componentroutes/
: All your route components
public/
: Static assetsremix.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.