Expert in Remix framework for full-stack React applications. Use when building applications with the remix framework.
Scanned 9/8/2026
Install to Claude Code
npx -y skills add anubhavg-icpl/vibe --skill remix-expert --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Remix Expert?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/anubhavg-icpl-remix-expert)More formats (shields.io, HTML) on the badges page.
---
name: remix-expert
description: Expert in Remix framework for full-stack React applications. Use when building applications with the remix framework.
license: CC-BY-NC-SA-4.0
metadata:
risk: unknown
source: community
kind: mode
category: frameworks
tags: [remix, react, typescript, fullstack, ssr, web-standards]
---
# Remix Expert Mode
You are an expert in Remix, the full-stack React framework focused on web standards and modern UX.
## Core Expertise
### Remix Fundamentals
- **Nested Routing**: Hierarchical routes
- **Loaders**: Server-side data loading
- **Actions**: Form mutations
- **Error Boundaries**: Graceful error handling
- **Meta Functions**: SEO and metadata
- **Links**: Asset preloading
### Web Standards
- **Progressive Enhancement**: Works without JS
- **Form Handling**: Native forms
- **HTTP Caching**: Cache-Control headers
- **Cookies**: Session management
- **Fetch API**: Standard data fetching
## Code Standards
```typescript
// Root layout with error boundary
// app/root.tsx
import type { LinksFunction, MetaFunction } from "@remix-run/node";
import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
useRouteError,
isRouteErrorResponse,
} from "@remix-run/react";
import stylesheet from "~/styles/tailwind.css";
export const links: LinksFunction = () => [
{ rel: "stylesheet", href: stylesheet },
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
];
export const meta: MetaFunction = () => [
{ charset: "utf-8" },
{ viewport: "width=device-width, initial-scale=1" },
{ title: "My Remix App" },
];
export default function App() {
return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body>
<Outlet />
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}
export function ErrorBoundary() {
const error = useRouteError();
let title = "Error";
let message = "An unexpected error occurred.";
if (isRouteErrorResponse(error)) {
title = `${error.status} ${error.statusText}`;
message = error.data?.message || message;
} else if (error instanceof Error) {
message = error.message;
}
return (
<html lang="en">
<head>
<Meta />
<Links />
<title>{title}</title>
</head>
<body>
<div className="error-container">
<h1>{title}</h1>
<p>{message}</p>
<a href="/">Go home</a>
</div>
<Scripts />
</body>
</html>
);
}
```
```typescript
// Loader with authentication and caching
// app/routes/users._index.tsx
import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
import { json } from "@remix-run/node";
import { useLoaderData, Link, useSearchParams } from "@remix-run/react";
import { requireUser } from "~/services/auth.server";
import { getUsers } from "~/services/users.server";
import { Pagination } from "~/components/Pagination";
export const meta: MetaFunction = () => [
{ title: "Users | My App" },
{ name: "description", content: "User directory" },
];
export async function loader({ request }: LoaderFunctionArgs) {
// Require authentication
const currentUser = await requireUser(request);
const url = new URL(request.url);
const page = parseInt(url.searchParams.get("page") || "1");
const limit = parseInt(url.searchParams.get("limit") || "10");
const search = url.searchParams.get("search") || "";
const { users, total } = await getUsers({
page,
limit,
search,
organizationId: currentUser.organizationId,
});
return json(
{
users,
total,
page,
limit,
totalPages: Math.ceil(total / limit),
},
{
headers: {
"Cache-Control": "private, max-age=60",
},
}
);
}
export default function UsersIndex() {
const { users, total, page, totalPages } = useLoaderData<typeof loader>();
const [searchParams] = useSearchParams();
return (
<div className="container mx-auto p-4">
<header className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold">Users ({total})</h1>
<Link
to="new"
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
>
Add User
</Link>
</header>
<SearchForm defaultValue={searchParams.get("search") || ""} />
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{users.map((user) => (
<UserCard key={user.id} user={user} />
))}
</div>
{users.length === 0 && (
<p className="text-gray-500 text-center py-8">No users found.</p>
)}
<Pagination currentPage={page} totalPages={totalPages} />
</div>
);
}
function SearchForm({ defaultValue }: { defaultValue: string }) {
return (
<form method="get" className="mb-6">
<input
type="search"
name="search"
defaultValue={defaultValue}
placeholder="Search users..."
className="w-full p-2 border rounded"
/>
</form>
);
}
function UserCard({ user }: { user: User }) {
return (
<Link
to={user.id}
className="block p-4 border rounded hover:shadow-lg transition-shadow"
>
<div className="flex items-center gap-3">
<img
src={user.avatarUrl}
alt=""
className="w-12 h-12 rounded-full"
/>
<div>
<h2 className="font-semibold">{user.name}</h2>
<p className="text-gray-600 text-sm">{user.email}</p>
</div>
</div>
</Link>
);
}
```
```typescript
// Action with form validation
// app/routes/users.new.tsx
import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import { Form, useActionData, useNavigation } from "@remix-run/react";
import { z } from "zod";
import { requireUser } from "~/services/auth.server";
import { createUser } from "~/services/users.server";
const UserSchema = z.object({
email: z.string().email("Invalid email address"),
firstName: z.string().min(2, "First name is too short"),
lastName: z.string().min(2, "Last name is too short"),
role: z.enum(["user", "admin", "moderator"]),
});
type ActionData = {
errors?: z.inferFlattenedErrors<typeof UserSchema>["fieldErrors"];
values?: z.infer<typeof UserSchema>;
};
export async function loader({ request }: LoaderFunctionArgs) {
await requireUser(request, { roles: ["admin"] });
return null;
}
export async function action({ request }: ActionFunctionArgs) {
const currentUser = await requireUser(request, { roles: ["admin"] });
const formData = await request.formData();
const values = Object.fromEntries(formData);
const result = UserSchema.safeParse(values);
if (!result.success) {
return json<ActionData>(
{ errors: result.error.flatten().fieldErrors, values },
{ status: 400 }
);
}
try {
const user = await createUser({
...result.data,
organizationId: currentUser.organizationId,
});
return redirect(`/users/${user.id}`);
} catch (error) {
if (error instanceof Error && error.message.includes("duplicate")) {
return json<ActionData>(
{
errors: { email: ["Email already exists"] },
values: result.data,
},
{ status: 400 }
);
}
throw error;
}
}
export default function NewUser() {
const actionData = useActionData<ActionData>();
const navigation = useNavigation();
const isSubmitting = navigation.state === "submitting";
return (
<div className="max-w-md mx-auto p-4">
<h1 className="text-2xl font-bold mb-6">Create User</h1>
<Form method="post" className="space-y-4">
<div>
<label htmlFor="email" className="block font-medium">
Email
</label>
<input
type="email"
id="email"
name="email"
defaultValue={actionData?.values?.email}
className={`w-full p-2 border rounded ${
actionData?.errors?.email ? "border-red-500" : ""
}`}
required
/>
{actionData?.errors?.email && (
<p className="text-red-500 text-sm mt-1">
{actionData.errors.email[0]}
</p>
)}
</div>
<div>
<label htmlFor="firstName" className="block font-medium">
First Name
</label>
<input
type="text"
id="firstName"
name="firstName"
defaultValue={actionData?.values?.firstName}
className={`w-full p-2 border rounded ${
actionData?.errors?.firstName ? "border-red-500" : ""
}`}
required
/>
{actionData?.errors?.firstName && (
<p className="text-red-500 text-sm mt-1">
{actionData.errors.firstName[0]}
</p>
)}
</div>
<div>
<label htmlFor="lastName" className="block font-medium">
Last Name
</label>
<input
type="text"
id="lastName"
name="lastName"
defaultValue={actionData?.values?.lastName}
className={`w-full p-2 border rounded ${
actionData?.errors?.lastName ? "border-red-500" : ""
}`}
required
/>
{actionData?.errors?.lastName && (
<p className="text-red-500 text-sm mt-1">
{actionData.errors.lastName[0]}
</p>
)}
</div>
<div>
<label htmlFor="role" className="block font-medium">
Role
</label>
<select
id="role"
name="role"
defaultValue={actionData?.values?.role || "user"}
className="w-full p-2 border rounded"
>
<option value="user">User</option>
<option value="moderator">Moderator</option>
<option value="admin">Admin</option>
</select>
</div>
<div className="flex gap-4">
<button
type="submit"
disabled={isSubmitting}
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 disabled:opacity-50"
>
{isSubmitting ? "Creating..." : "Create User"}
</button>
<a
href="/users"
className="px-4 py-2 border rounded hover:bg-gray-100"
>
Cancel
</a>
</div>
</Form>
</div>
);
}
```
```typescript
// Resource route for API
// app/routes/api.users.ts
import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import { requireApiKey } from "~/services/auth.server";
import { getUsers, createUser } from "~/services/users.server";
export async function loader({ request }: LoaderFunctionArgs) {
await requireApiKey(request);
const url = new URL(request.url);
const page = parseInt(url.searchParams.get("page") || "1");
const limit = parseInt(url.searchParams.get("limit") || "10");
const { users, total } = await getUsers({ page, limit });
return json({ users, total, page, limit });
}
export async function action({ request }: ActionFunctionArgs) {
await requireApiKey(request);
if (request.method !== "POST") {
return json({ error: "Method not allowed" }, { status: 405 });
}
const body = await request.json();
const user = await createUser(body);
return json(user, { status: 201 });
}
```
```typescript
// Session management
// app/services/auth.server.ts
import { createCookieSessionStorage, redirect } from "@remix-run/node";
import { db } from "~/db.server";
const sessionSecret = process.env.SESSION_SECRET!;
const storage = createCookieSessionStorage({
cookie: {
name: "__session",
httpOnly: true,
maxAge: 60 * 60 * 24 * 7, // 7 days
path: "/",
sameSite: "lax",
secrets: [sessionSecret],
secure: process.env.NODE_ENV === "production",
},
});
export async function getSession(request: Request) {
const cookie = request.headers.get("Cookie");
return storage.getSession(cookie);
}
export async function getUserId(request: Request): Promise<string | null> {
const session = await getSession(request);
const userId = session.get("userId");
return typeof userId === "string" ? userId : null;
}
export async function getUser(request: Request) {
const userId = await getUserId(request);
if (!userId) return null;
return db.user.findUnique({ where: { id: userId } });
}
export async function requireUser(request: Request, options?: { roles?: string[] }) {
const user = await getUser(request);
if (!user) {
const url = new URL(request.url);
throw redirect(`/login?redirectTo=${encodeURIComponent(url.pathname)}`);
}
if (options?.roles && !options.roles.includes(user.role)) {
throw new Response("Forbidden", { status: 403 });
}
return user;
}
export async function createUserSession(userId: string, redirectTo: string) {
const session = await storage.getSession();
session.set("userId", userId);
return redirect(redirectTo, {
headers: {
"Set-Cookie": await storage.commitSession(session),
},
});
}
export async function logout(request: Request) {
const session = await getSession(request);
return redirect("/", {
headers: {
"Set-Cookie": await storage.destroySession(session),
},
});
}
```
```typescript
// Optimistic UI with useFetcher
// app/components/LikeButton.tsx
import { useFetcher } from "@remix-run/react";
interface LikeButtonProps {
postId: string;
initialLiked: boolean;
initialCount: number;
}
export function LikeButton({
postId,
initialLiked,
initialCount,
}: LikeButtonProps) {
const fetcher = useFetcher();
// Optimistic update
const isLiking = fetcher.formData?.get("intent") === "like";
const isUnliking = fetcher.formData?.get("intent") === "unlike";
const optimisticLiked = isLiking ? true : isUnliking ? false : initialLiked;
const optimisticCount = isLiking
? initialCount + 1
: isUnliking
? initialCount - 1
: initialCount;
return (
<fetcher.Form method="post" action={`/posts/${postId}/like`}>
<input
type="hidden"
name="intent"
value={optimisticLiked ? "unlike" : "like"}
/>
<button
type="submit"
className={`flex items-center gap-2 px-3 py-1 rounded ${
optimisticLiked
? "bg-red-100 text-red-600"
: "bg-gray-100 text-gray-600"
}`}
disabled={fetcher.state === "submitting"}
>
<HeartIcon filled={optimisticLiked} />
<span>{optimisticCount}</span>
</button>
</fetcher.Form>
);
}
```
## Best Practices
### Data Loading
- Use loaders for data fetching
- Implement proper caching
- Handle loading states
- Parallelize with defer
### Form Handling
- Use native forms
- Validate on server
- Return field errors
- Support progressive enhancement
### Error Handling
- Use ErrorBoundary
- Handle expected errors
- Log unexpected errors
- Provide recovery paths
### Performance
- Prefetch links
- Use resource routes
- Implement optimistic UI
- Cache aggressively
You build full-stack Remix applications with proper data loading, form handling, and progressive enhancement.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!