remix-patterns

Remix Development Patterns

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "remix-patterns" with this command: npx skills add tejovanthn/rasikalife/tejovanthn-rasikalife-remix-patterns

Remix Development Patterns

This skill provides comprehensive guidance for building Remix applications following best practices and framework conventions.

Core Philosophy

Remix embraces:

  • Web Fundamentals: Work with HTTP, not against it

  • Progressive Enhancement: Apps should work without JavaScript

  • Server-First: Do the work on the server, send HTML

  • Nested Routes: Compose UIs with route hierarchy

  • No Client State: Use the URL and server as the source of truth

Route Structure

Basic Route Module

Every route module can export these functions:

// app/routes/posts.$id.tsx import type { LoaderFunctionArgs, ActionFunctionArgs } from "@remix-run/node"; import { json } from "@remix-run/node"; import { useLoaderData, Form } from "@remix-run/react";

// Loader: Fetch data on GET requests export async function loader({ params, request }: LoaderFunctionArgs) { const post = await getPost(params.id); if (!post) { throw new Response("Not Found", { status: 404 }); } return json({ post }); }

// Action: Handle mutations (POST, PUT, DELETE) export async function action({ request, params }: ActionFunctionArgs) { const formData = await request.formData(); const title = formData.get("title");

await updatePost(params.id, { title }); return json({ success: true }); }

// Component: Render the UI export default function Post() { const { post } = useLoaderData<typeof loader>();

return ( <div> <h1>{post.title}</h1> <Form method="post"> <input name="title" defaultValue={post.title} /> <button type="submit">Update</button> </Form> </div> ); }

Data Loading Patterns

Pattern 1: Simple Data Fetching

export async function loader({ params }: LoaderFunctionArgs) { const [user, posts] = await Promise.all([ db.user.findUnique({ where: { id: params.userId } }), db.post.findMany({ where: { userId: params.userId } }) ]);

return json({ user, posts }); }

Pattern 2: Authentication Checks

export async function loader({ request }: LoaderFunctionArgs) { const userId = await requireAuth(request); const user = await db.user.findUnique({ where: { id: userId } });

return json({ user }); }

// Helper function async function requireAuth(request: Request) { const userId = await getUserId(request); if (!userId) { throw redirect("/login"); } return userId; }

Pattern 3: Search with URL Params

export async function loader({ request }: LoaderFunctionArgs) { const url = new URL(request.url); const query = url.searchParams.get("q") || ""; const page = Number(url.searchParams.get("page")) || 1;

const results = await searchPosts({ query, page });

return json({ results, query, page }); }

export default function Search() { const { results, query } = useLoaderData<typeof loader>();

return ( <div> <Form method="get"> <input name="q" defaultValue={query} /> <button type="submit">Search</button> </Form> {/* Results */} </div> ); }

Pattern 4: Dependent Loaders (Parent/Child)

// app/routes/users.$userId.tsx (parent) export async function loader({ params }: LoaderFunctionArgs) { const user = await db.user.findUnique({ where: { id: params.userId } }); return json({ user }); }

// app/routes/users.$userId.posts.tsx (child) export async function loader({ params }: LoaderFunctionArgs) { // Can access parent data via useRouteLoaderData const posts = await db.post.findMany({ where: { userId: params.userId } }); return json({ posts }); }

export default function UserPosts() { const { posts } = useLoaderData<typeof loader>(); const { user } = useRouteLoaderData<typeof parentLoader>("routes/users.$userId");

return <div>{/* ... */}</div>; }

Form Handling Patterns

Pattern 1: Basic Form Submission

export async function action({ request }: ActionFunctionArgs) { const formData = await request.formData(); const title = formData.get("title"); const content = formData.get("content");

const post = await db.post.create({ data: { title, content } });

return redirect(/posts/${post.id}); }

export default function NewPost() { return ( <Form method="post"> <input name="title" required /> <textarea name="content" required /> <button type="submit">Create Post</button> </Form> ); }

Pattern 2: Form with Validation

import { z } from "zod";

const schema = z.object({ email: z.string().email(), password: z.string().min(8) });

export async function action({ request }: ActionFunctionArgs) { const formData = await request.formData(); const data = Object.fromEntries(formData);

const result = schema.safeParse(data); if (!result.success) { return json( { errors: result.error.flatten() }, { status: 400 } ); }

await createUser(result.data); return redirect("/dashboard"); }

export default function Signup() { const actionData = useActionData<typeof action>();

return ( <Form method="post"> <input name="email" /> {actionData?.errors.fieldErrors.email && ( <span>{actionData.errors.fieldErrors.email}</span> )}

  &#x3C;input name="password" type="password" />
  {actionData?.errors.fieldErrors.password &#x26;&#x26; (
    &#x3C;span>{actionData.errors.fieldErrors.password}&#x3C;/span>
  )}
  
  &#x3C;button type="submit">Sign Up&#x3C;/button>
&#x3C;/Form>

); }

Pattern 3: Optimistic UI

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

export default function Todo({ todo }) { const fetcher = useFetcher();

// Optimistic state const isComplete = fetcher.formData ? fetcher.formData.get("complete") === "true" : todo.complete;

return ( <fetcher.Form method="post" action={/todos/${todo.id}}> <input type="hidden" name="complete" value={String(!isComplete)} /> <button type="submit"> {isComplete ? "✓" : "○"} {todo.title} </button> </fetcher.Form> ); }

Pattern 4: Multiple Actions per Route

export async function action({ request }: ActionFunctionArgs) { const formData = await request.formData(); const intent = formData.get("intent");

switch (intent) { case "delete": await deletePost(formData.get("id")); return json({ success: true });

case "update":
  await updatePost(formData.get("id"), {
    title: formData.get("title")
  });
  return json({ success: true });
  
default:
  throw new Response("Invalid intent", { status: 400 });

} }

export default function Post() { return ( <> <Form method="post"> <input type="hidden" name="intent" value="update" /> <input name="title" /> <button type="submit">Update</button> </Form>

  &#x3C;Form method="post">
    &#x3C;input type="hidden" name="intent" value="delete" />
    &#x3C;button type="submit">Delete&#x3C;/button>
  &#x3C;/Form>
&#x3C;/>

); }

Error Handling

Pattern 1: Error Boundaries

// app/routes/posts.$id.tsx export async function loader({ params }: LoaderFunctionArgs) { const post = await db.post.findUnique({ where: { id: params.id } });

if (!post) { throw new Response("Post not found", { status: 404 }); }

return json({ post }); }

export function ErrorBoundary() { const error = useRouteError();

if (isRouteErrorResponse(error)) { return ( <div> <h1>{error.status} {error.statusText}</h1> <p>{error.data}</p> </div> ); }

return <div>Something went wrong!</div>; }

Pattern 2: Nested Error Boundaries

// Root error boundary (app/root.tsx) export function ErrorBoundary() { return ( <html> <body> <h1>Application Error</h1> <p>Sorry, something went wrong.</p> </body> </html> ); }

// Route-specific error boundary export function ErrorBoundary() { return ( <div> <h1>Post Error</h1> <p>Couldn't load this post.</p> </div> ); }

Nested Routes

Pattern 1: Layout Routes

// app/routes/dashboard.tsx (layout) import { Outlet } from "@remix-run/react";

export default function Dashboard() { return ( <div> <nav>{/* Dashboard navigation /}</nav> <main> <Outlet /> {/ Child routes render here */} </main> </div> ); }

// app/routes/dashboard.settings.tsx (child) export default function Settings() { return <div>Settings content</div>; }

// app/routes/dashboard.profile.tsx (child) export default function Profile() { return <div>Profile content</div>; }

Pattern 2: Pathless Layouts

// app/routes/_auth.tsx (pathless layout - note the underscore) export default function AuthLayout() { return ( <div className="auth-container"> <Outlet /> </div> ); }

// app/routes/_auth.login.tsx // URL: /login (not /_auth/login) export default function Login() { return <Form>{/* login form */}</Form>; }

// app/routes/_auth.signup.tsx // URL: /signup export default function Signup() { return <Form>{/* signup form */}</Form>; }

Resource Routes

Pattern 1: Image Generation

// app/routes/og.$id.tsx import { LoaderFunctionArgs } from "@remix-run/node";

export async function loader({ params }: LoaderFunctionArgs) { const post = await getPost(params.id); const image = await generateOGImage(post);

return new Response(image, { headers: { "Content-Type": "image/png", "Cache-Control": "public, max-age=31536000, immutable" } }); }

Pattern 2: File Downloads

// app/routes/downloads.$fileId.tsx export async function loader({ params }: LoaderFunctionArgs) { const file = await getFile(params.fileId); const stream = await getFileStream(file.path);

return new Response(stream, { headers: { "Content-Type": file.mimeType, "Content-Disposition": attachment; filename="${file.name}" } }); }

Best Practices

  1. Prefer Server-Side Logic

❌ Don't fetch on the client:

export default function Posts() { const [posts, setPosts] = useState([]);

useEffect(() => { fetch("/api/posts").then(r => r.json()).then(setPosts); }, []); }

✅ Do use loaders:

export async function loader() { const posts = await db.post.findMany(); return json({ posts }); }

export default function Posts() { const { posts } = useLoaderData<typeof loader>(); return <div>{/* render posts */}</div>; }

  1. Use Form Component

❌ Don't use fetch for mutations:

function handleSubmit(e) { e.preventDefault(); fetch("/api/posts", { method: "POST", body: formData }); }

✅ Do use Form:

export default function NewPost() { return ( <Form method="post"> <input name="title" /> <button type="submit">Create</button> </Form> ); }

  1. Colocate Related Code

✅ Keep route concerns together:

// app/routes/posts.$id.tsx // - loader (data fetching) // - action (mutations) // - component (UI) // - ErrorBoundary (error handling) // All in one file!

  1. Use Type Safety

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

export async function loader({ params }: LoaderFunctionArgs) { // TypeScript knows about params, request, etc. }

export default function Component() { // Automatic type inference from loader! const data = useLoaderData<typeof loader>(); // ^? data is typed based on loader return }

  1. Handle Loading States

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

export default function Component() { const navigation = useNavigation(); const isSubmitting = navigation.state === "submitting";

return ( <Form method="post"> <button disabled={isSubmitting}> {isSubmitting ? "Saving..." : "Save"} </button> </Form> ); }

Common Gotchas

  1. Loader Return Types

Always use json() or Response :

✅ return json({ data }); ✅ return new Response(data); ❌ return { data }; // Won't work correctly

  1. Form Data is Always Strings

const formData = await request.formData(); const age = formData.get("age"); // This is a string!

// Convert to number const ageNumber = Number(age);

  1. Redirects Stop Execution

export async function action({ request }: ActionFunctionArgs) { if (!isValid) { return json({ error: "Invalid" }); // Continues execution }

throw redirect("/login"); // Stops execution immediately }

Performance Tips

  1. Parallel Data Loading

export async function loader() { // These load in parallel! const [user, posts, comments] = await Promise.all([ getUser(), getPosts(), getComments() ]);

return json({ user, posts, comments }); }

  1. Cache Headers

export async function loader() { const data = await getStaticData();

return json(data, { headers: { "Cache-Control": "public, max-age=3600" } }); }

  1. Defer for Streaming

import { defer } from "@remix-run/node"; import { Await, useLoaderData } from "@remix-run/react"; import { Suspense } from "react";

export async function loader() { return defer({ critical: await getCriticalData(), // Wait for this deferred: getDeferredData() // Don't wait }); }

export default function Component() { const { critical, deferred } = useLoaderData<typeof loader>();

return ( <div> <div>{critical}</div>

  &#x3C;Suspense fallback={&#x3C;div>Loading...&#x3C;/div>}>
    &#x3C;Await resolve={deferred}>
      {(data) => &#x3C;div>{data}&#x3C;/div>}
    &#x3C;/Await>
  &#x3C;/Suspense>
&#x3C;/div>

); }

Further Reading

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

sst-dev

No summary provided by upstream source.

Repository SourceNeeds Review
General

frontend-design

No summary provided by upstream source.

Repository SourceNeeds Review
General

email-templates

No summary provided by upstream source.

Repository SourceNeeds Review
General

marketing-copy

No summary provided by upstream source.

Repository SourceNeeds Review