Remix takes a fundamentally different approach to data loading and mutations. Instead of fetching from API endpoints, Remix uses loaders and actions that run on the server — and it's incredibly powerful.
Loaders — Server-Side Data Fetching
Every route can export a loader function that runs on the server before the component renders:
// app/routes/products.jsx
import { json } from "@remix-run/node"
import { useLoaderData } from "@remix-run/react"
export async function loader({ request }) {
const url = new URL(request.url)
const search = url.searchParams.get("q") || ""
const products = await db.product.findMany({
where: { name: { contains: search } },
take: 20
})
return json({ products, search })
}
export default function Products() {
const { products, search } = useLoaderData()
return (
<div>
<h1>Products</h1>
{products.map(p => <ProductCard key={p.id} product={p} />)}
</div>
)
}
Actions — Server-Side Mutations
Actions handle form submissions and POST/PUT/DELETE requests:
// app/routes/products.new.jsx
import { json, redirect } from "@remix-run/node"
import { Form, useActionData } from "@remix-run/react"
export async function action({ request }) {
const formData = await request.formData()
const name = formData.get("name")
const price = parseFloat(formData.get("price"))
const errors = {}
if (!name) errors.name = "Name is required"
if (isNaN(price)) errors.price = "Valid price required"
if (Object.keys(errors).length) {
return json({ errors }, { status: 400 })
}
const product = await db.product.create({ data: { name, price } })
return redirect(`/products/${product.id}`)
}
export default function NewProduct() {
const actionData = useActionData()
return (
<Form method="post">
<input name="name" placeholder="Product name" />
{actionData?.errors?.name && <p>{actionData.errors.name}</p>}
<input name="price" type="number" step="0.01" />
{actionData?.errors?.price && <p>{actionData.errors.price}</p>}
<button type="submit">Create</button>
</Form>
)
}
Resource Routes — Pure API Endpoints
Routes without a default export become API-only:
// app/routes/api.users.jsx
import { json } from "@remix-run/node"
export async function loader({ request }) {
const users = await db.user.findMany()
return json(users, {
headers: {
"Cache-Control": "public, max-age=300"
}
})
}
export async function action({ request }) {
const body = await request.json()
const user = await db.user.create({ data: body })
return json(user, { status: 201 })
}
Nested Routes & Parallel Loading
Remix loads data for all nested routes in parallel:
app/routes/
dashboard.jsx → loads user data
dashboard.stats.jsx → loads stats (parallel!)
dashboard.recent.jsx → loads recent items (parallel!)
// app/routes/dashboard.jsx
export async function loader({ request }) {
const user = await getUser(request)
return json({ user })
}
// app/routes/dashboard.stats.jsx
export async function loader() {
const stats = await getStats() // Runs IN PARALLEL with parent
return json({ stats })
}
Streaming with defer()
Stream slow data while showing fast data immediately:
import { defer } from "@remix-run/node"
import { Await, useLoaderData } from "@remix-run/react"
import { Suspense } from "react"
export async function loader() {
const fastData = await getFastData()
const slowDataPromise = getSlowData() // Don't await!
return defer({
fast: fastData,
slow: slowDataPromise
})
}
export default function Page() {
const { fast, slow } = useLoaderData()
return (
<div>
<h1>{fast.title}</h1>
<Suspense fallback={<p>Loading...</p>}>
<Await resolve={slow}>
{(data) => <SlowComponent data={data} />}
</Await>
</Suspense>
</div>
)
}
Key Takeaways
- Loaders fetch data server-side before rendering
- Actions handle mutations with progressive enhancement
- Resource Routes are pure API endpoints
- Nested Routes load data in parallel automatically
- defer() streams slow data progressively
- Everything works without JavaScript enabled (forms!)
Explore Remix docs for the complete API.
Building web scrapers or data pipelines? Check out my Apify actors for ready-made solutions, or email spinov001@gmail.com for custom development.
Top comments (0)