Remix is the full-stack web framework built on web standards — now part of React Router v7. It gives you server-side rendering, nested routing, and progressive enhancement without the complexity of Next.js.
Why Remix?
- Web standards first — uses fetch, Request, Response, FormData natively
- Nested routing — parallel data loading, no waterfalls
- Progressive enhancement — forms work without JavaScript
- No client-side state management — the URL is your state
- Error boundaries — per-route error handling
- Streaming — defer non-critical data for faster first paint
Quick Start
npx create-remix@latest my-app
cd my-app
npm run dev
Route-Based Architecture
app/
routes/
_index.tsx → /
about.tsx → /about
dashboard.tsx → /dashboard (layout)
dashboard._index.tsx → /dashboard/
dashboard.settings.tsx → /dashboard/settings
blog.$slug.tsx → /blog/:slug (dynamic)
$.tsx → catch-all (404)
Loaders (Server-Side Data)
// app/routes/users.tsx
import type { LoaderFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
// This runs on the SERVER — never sent to the browser
export async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url);
const search = url.searchParams.get("search") || "";
const users = await db.user.findMany({
where: { name: { contains: search } },
select: { id: true, name: true, email: true },
});
return json({ users, search });
}
// This runs on the CLIENT
export default function Users() {
const { users, search } = useLoaderData<typeof loader>();
return (
<div>
<h1>Users</h1>
<form method="get">
<input name="search" defaultValue={search} placeholder="Search..." />
<button type="submit">Search</button>
</form>
<ul>
{users.map((user) => (
<li key={user.id}>{user.name} — {user.email}</li>
))}
</ul>
</div>
);
}
Actions (Form Handling)
// app/routes/contacts.new.tsx
import type { ActionFunctionArgs } from "@remix-run/node";
import { redirect, json } from "@remix-run/node";
import { useActionData, Form } from "@remix-run/react";
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const name = formData.get("name") as string;
const email = formData.get("email") as string;
const errors: Record<string, string> = {};
if (!name) errors.name = "Name is required";
if (!email?.includes("@")) errors.email = "Valid email required";
if (Object.keys(errors).length > 0) {
return json({ errors }, { status: 400 });
}
await db.contact.create({ data: { name, email } });
return redirect("/contacts");
}
export default function NewContact() {
const actionData = useActionData<typeof action>();
return (
<Form method="post">
<div>
<label>Name</label>
<input name="name" />
{actionData?.errors?.name && <span>{actionData.errors.name}</span>}
</div>
<div>
<label>Email</label>
<input name="email" type="email" />
{actionData?.errors?.email && <span>{actionData.errors.email}</span>}
</div>
<button type="submit">Create Contact</button>
</Form>
);
}
This form works without JavaScript enabled — progressive enhancement.
Nested Routes (Parallel Data Loading)
// app/routes/dashboard.tsx (layout)
export async function loader() {
const user = await getUser();
return json({ user });
}
export default function Dashboard() {
const { user } = useLoaderData<typeof loader>();
return (
<div className="flex">
<nav>Welcome, {user.name}</nav>
<main>
<Outlet /> {/* Child routes render here */}
</main>
</div>
);
}
// app/routes/dashboard.analytics.tsx (child)
export async function loader() {
// This loads IN PARALLEL with parent loader!
const analytics = await getAnalytics();
return json({ analytics });
}
export default function Analytics() {
const { analytics } = useLoaderData<typeof loader>();
return <div>Views: {analytics.views}</div>;
}
Streaming (Deferred Data)
import { defer } from "@remix-run/node";
import { Await, useLoaderData } from "@remix-run/react";
import { Suspense } from "react";
export async function loader() {
// Critical data — wait for it
const product = await getProduct();
// Non-critical — stream it later
const reviewsPromise = getReviews(); // Don't await!
const recommendationsPromise = getRecommendations();
return defer({
product,
reviews: reviewsPromise,
recommendations: recommendationsPromise,
});
}
export default function Product() {
const { product, reviews, recommendations } = useLoaderData<typeof loader>();
return (
<div>
<h1>{product.name}</h1>
<p>${product.price}</p>
<Suspense fallback={<div>Loading reviews...</div>}>
<Await resolve={reviews}>
{(reviews) => (
<ul>{reviews.map((r) => <li key={r.id}>{r.text}</li>)}</ul>
)}
</Await>
</Suspense>
</div>
);
}
Error Boundaries
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
<div>
<h1>{error.status}</h1>
<p>{error.statusText}</p>
</div>
);
}
return <div>Something went wrong</div>;
}
Each route has its own error boundary — errors don't crash the whole app.
Remix vs Next.js vs SvelteKit
| Feature | Remix | Next.js | SvelteKit |
|---|---|---|---|
| Data loading | Loaders (web fetch) | getServerSideProps/RSC | load functions |
| Forms | Progressive enhancement | Client-side | Progressive enhancement |
| Nested routes | Built-in (parallel) | Layout groups | Layout groups |
| Streaming | defer + Suspense | RSC streaming | Deferred |
| State management | URL + server | Client state libs | URL + stores |
| Deploy | Anywhere | Vercel-optimized | Anywhere |
Need to scrape data from any website and get it in structured JSON? Check out my web scraping tools on Apify — no coding required, results in minutes.
Have a custom data extraction project? Email me at spinov001@gmail.com — I build tailored scraping solutions for businesses.
Top comments (0)