| name | route |
| description | Create Remix routes with loaders and actions. Use when creating new pages, implementing route loaders/actions, handling navigation, or when the user mentions route, page, loader, action, or navigation. |
| allowed-tools | ["Read","Write","Edit","Glob","Grep"] |
Creating Remix Routes
Create routes following Pixel Studio's Remix conventions.
Route File Naming
Routes in app/routes/ use dot-separated naming:
_index.tsx # / (home)
explore.tsx # /explore
user.$userId.tsx # /user/:userId (dynamic segment)
generate.tsx # /generate
generate_.tsx # /generate (no layout nesting)
auth.login.tsx # /auth/login
api.images.ts # /api/images (API route)
Full Page Route Template
import { json, type LoaderFunctionArgs, type ActionFunctionArgs, type MetaFunction } from "@remix-run/node";
import { useLoaderData, useActionData } from "@remix-run/react";
import { z } from "zod";
import { requireUserLogin } from "~/server/auth.server";
import { getResource, createResource } from "~/server/resource.server";
export const meta: MetaFunction = () => {
return [
{ title: "Page Title | Pixel Studio" },
{ name: "description", content: "Page description" },
];
};
export async function loader({ request }: LoaderFunctionArgs) {
const user = await requireUserLogin(request);
const resources = await getResource(user.id);
return json({ resources, user });
}
const ActionSchema = z.object({
title: z.string().min(1, "Title required"),
});
export async function action({ request }: ActionFunctionArgs) {
const user = await requireUserLogin(request);
const formData = await request.formData();
const result = ActionSchema.safeParse(Object.fromEntries(formData));
if (!result.success) {
return json({ error: result.error.errors[0].message }, { status: 400 });
}
await createResource(user.id, result.data);
return json({ success: true });
}
export default function ResourcePage() {
const { resources, user } = useLoaderData<typeof loader>();
const actionData = useActionData<typeof action>();
return (
<div className="container mx-auto py-8">
<h1 className="text-2xl font-bold mb-6">Resources</h1>
{actionData?.error && (
<p className="text-red-500 mb-4">{actionData.error}</p>
)}
{/* Page content */}
</div>
);
}
export function ErrorBoundary() {
return (
<div className="container mx-auto py-8">
<h1 className="text-2xl font-bold text-red-500">Error</h1>
<p>Something went wrong loading this page.</p>
</div>
);
}
Loader Patterns
Public Route with Optional Auth
export async function loader({ request }: LoaderFunctionArgs) {
const auth = await getGoogleSessionAuth(request);
const userId = auth?.user?.id;
const images = await getPublicImages();
return json({ images, userId });
}
Protected Route
export async function loader({ request }: LoaderFunctionArgs) {
const user = await requireUserLogin(request);
return json({ user });
}
Dynamic Parameters
export async function loader({ request, params }: LoaderFunctionArgs) {
const { userId } = params;
if (!userId) {
throw new Response("User ID required", { status: 400 });
}
const user = await getUserProfile(userId);
if (!user) {
throw new Response("User not found", { status: 404 });
}
return json({ user });
}
URL Search Params
export async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url);
const page = parseInt(url.searchParams.get("page") || "1");
const search = url.searchParams.get("q") || "";
const { images, total } = await getImages({ page, search });
return json({ images, total, page, search });
}
Action Patterns
Multiple Intents
export async function action({ request }: ActionFunctionArgs) {
const user = await requireUserLogin(request);
const formData = await request.formData();
const intent = formData.get("intent");
switch (intent) {
case "create": {
const title = formData.get("title") as string;
await createResource(user.id, { title });
return json({ success: true });
}
case "delete": {
const id = formData.get("id") as string;
await deleteResource(id, user.id);
return json({ success: true });
}
default:
return json({ error: "Invalid intent" }, { status: 400 });
}
}
Redirect After Action
import { redirect } from "@remix-run/node";
export async function action({ request }: ActionFunctionArgs) {
const user = await requireUserLogin(request);
return redirect(`/resource/${newResource.id}`);
}
Component Patterns
Using Loader Data
import { useLoaderData } from "@remix-run/react";
export default function Page() {
const { resources, user } = useLoaderData<typeof loader>();
}
Form Submission
import { Form, useNavigation } from "@remix-run/react";
export default function Page() {
const navigation = useNavigation();
const isSubmitting = navigation.state === "submitting";
return (
<Form method="post">
<input type="hidden" name="intent" value="create" />
<input name="title" required />
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Creating..." : "Create"}
</button>
</Form>
);
}
Programmatic Navigation
import { useNavigate, useSearchParams } from "@remix-run/react";
export default function Page() {
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const goToDetails = (id: string) => navigate(`/resource/${id}`);
const setPage = (page: number) => {
setSearchParams({ page: String(page) });
};
}
Layout Routes
Nested Layout
import { Outlet } from "@remix-run/react";
export default function DashboardLayout() {
return (
<div className="flex">
<aside className="w-64">Sidebar</aside>
<main className="flex-1">
<Outlet /> {/* Child routes render here */}
</main>
</div>
);
}
export default function Settings() {
return <div>Settings Page</div>;
}
Pathless Layout (Grouping)
Links and Navigation
import { Link, NavLink } from "@remix-run/react";
<Link to="/explore">Explore</Link>
<NavLink
to="/dashboard"
className={({ isActive }) =>
isActive ? "text-blue-500" : "text-gray-500"
}
>
Dashboard
</NavLink>
<Link to="/resource" prefetch="intent">
View Resource
</Link>
Checklist