Remix
What, why & how?
- What is Remix?
- How Remix works under the hood?
- Data Flow
- Mutations
- Routing
- Cool bits
- Resources
Agenda

What is Remix?
Focused on web standards and modern web app UX, you’re simply going to build better websites
Remix
What it is under the hood?
Compiler
Server-side HTTP handler
Server framework
Browser framework
Remix
What it is under the hood?
Compiler
Server-side HTTP handler
Server framework
Browser framework
Remix
What it is under the hood?
Compiler
Server-side HTTP handler
Server framework
Browser framework
Remix
What it is under the hood?
Compiler
Server-side HTTP handler
Server framework
Browser framework
Remix
What it is under the hood?
Compiler
Server-side HTTP handler
Server framework
Browser framework
Data Flow

Data Flow
export async function loader() {
// provides data to the component
}
export default function Component() {
// renders the UI
}
export async function action() {
// updates persistent data
}Data Flow
import type { LoaderFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
import { json } from "@remix-run/node"; // or cloudflare/deno
export async function loader({
request,
}: LoaderFunctionArgs) {
const user = await getUser(request);
return json({
displayName: user.displayName,
email: user.email,
});
}
export default function Component() {
// ...
}
export async function action() {
// ...
}
Data Flow
import type { LoaderFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
import { json } from "@remix-run/node"; // or cloudflare/deno
export async function loader({
request,
}: LoaderFunctionArgs) {
const user = await getUser(request);
return json({
displayName: user.displayName,
email: user.email,
});
}
export default function Component() {
// ...
}
export async function action() {
// ...
}
Data Flow
import type { LoaderFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
import { json } from "@remix-run/node"; // or cloudflare/deno
import { useLoaderData, Form } from "@remix-run/react";
export async function loader({
request,
}: LoaderFunctionArgs) {
const user = await getUser(request);
return json({
displayName: user.displayName,
email: user.email,
});
}
export default function Component() {
const user = useLoaderData<typeof loader>();
return (
<Form method="post" action="/account">
<h1>Settings for {user.displayName}</h1>
<input
name="displayName"
defaultValue={user.displayName}
/>
<input name="email" defaultValue={user.email} />
<button type="submit">Save</button>
</Form>
);
}
export async function action() {
// ...
}
Data Flow
import type {
ActionFunctionArgs,
LoaderFunctionArgs,
} from "@remix-run/node"; // or cloudflare/deno
import { json } from "@remix-run/node"; // or cloudflare/deno
import { useLoaderData, Form } from "@remix-run/react";
export async function loader({
request,
}: LoaderFunctionArgs) {
const user = await getUser(request);
return json({
displayName: user.displayName,
email: user.email,
});
}
export default function Component() {
const user = useLoaderData<typeof loader>();
return (
<Form method="post" action="/account">
<h1>Settings for {user.displayName}</h1>
<input
name="displayName"
defaultValue={user.displayName}
/>
<input name="email" defaultValue={user.email} />
<button type="submit">Save</button>
</Form>
);
}
export async function action({
request,
}: ActionFunctionArgs) {
const formData = await request.formData();
const user = await getUser(request);
await updateUser(user.id, {
email: formData.get("email"),
displayName: formData.get("displayName"),
});
return json({ ok: true });
}
Form vs Fetcher
Form
import type { ActionFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
import { redirect } from "@remix-run/node"; // or cloudflare/deno
import {
Form,
useActionData,
useNavigation,
} from "@remix-run/react";
export async function action({
request,
}: ActionFunctionArgs) {
const formData = await request.formData();
const errors = await validateRecipeFormData(formData);
if (errors) {
return json({ errors });
}
const recipe = await db.recipes.create(formData);
return redirect(`/recipes/${recipe.id}`);
}
export function NewRecipe() {
const { errors } = useActionData<typeof action>();
const navigation = useNavigation();
const isSubmitting =
navigation.formAction === "/recipes/new";
return (
<Form method="post">
<label>
Title: <input name="title" />
{errors?.title ? <span>{errors.title}</span> : null}
</label>
<label>
Ingredients: <textarea name="ingredients" />
{errors?.ingredients ? (
<span>{errors.ingredients}</span>
) : null}
</label>
<label>
Directions: <textarea name="directions" />
{errors?.directions ? (
<span>{errors.directions}</span>
) : null}
</label>
<button type="submit">
{isSubmitting ? "Saving..." : "Create Recipe"}
</button>
</Form>
);
}Form
- HTML <form> that submits data to actions via fetch
- idle, submitting, loading states via useLoading hook
- the browser manages the submission as well as the pending states
- most useful when you want to change the url after the submission
Fetcher
export async function action({
request,
}: ActionFunctionArgs) {
const formData = await request.formData();
const id = formData.get("id");
try {
await db.recipes.delete(id);
return json({ ok: true });
} catch(e: Error) {
return json({
error: e
}
})
}
const RecipeListItem: FunctionComponent<{
recipe: Recipe;
}> = ({ recipe }) => {
const fetcher = useFetcher<typeof action>();
const isDeleting = fetcher.state !== "idle";
const error = fethcer.data?.error;
return (
<li>
<h2>{recipe.title}</h2>
<fetcher.Form method="post">
<button disabled={isDeleting} type="submit">
{isDeleting ? "Deleting..." : "Delete"}
</button>
{error ?? <span>{error}</span>}
</fetcher.Form>
</li>
);
};Optimistic UI
Optimistic UI (delete)
export async function loader () {
const tasks = await prisma.task.findMany();
return json({ tasks });
};
export default function Tasks() {
const { tasks } = useLoaderData();
return (
<div>
<h1>Tasks</h1>
<ul>
{tasks.map(task => (
<li key={task.id}>{task.name}</li>
))}
</ul>
</div>
);
}Optimistic UI (delete)
export async function loader () {
const tasks = await prisma.task.findMany();
return json({ tasks });
};
export const action: ActionFunction = async ({ request }) => {
const formData = await request.formData();
const taskId = formData.get('taskId');
if (taskId) {
await prisma.task.delete({ where: { id: taskId } });
return json({});
}
return json({});
};
export default function Tasks() {
const { tasks } = useLoaderData();
return (
<div>
<h1>Tasks</h1>
<ul>
{tasks.map(task => (
<li key={task.id}>
{task.name}
<fetcher.Form method="post">
<input type="hidden" name="taskId" value={task.id} />
<button type="submit">×</button>
</fetcher.Form>
</li>
))}
</ul>
</div>
);
}
Optimistic UI (delete)
export async function loader () {
const tasks = await prisma.task.findMany();
return json({ tasks });
};
export const action: ActionFunction = async ({ request }) => {
const formData = await request.formData();
const taskId = formData.get('taskId');
if (taskId) {
await prisma.task.delete({ where: { id: taskId } });
return json({});
}
return json({});
};
export default function Tasks() {
const { tasks } = useLoaderData();
const fetcher = useFetcher();
return (
<div>
<h1>Tasks</h1>
<ul>
{tasks.map(task => (
<li key={task.id}>
{task.name}
<fetcher.Form method="post">
<input type="hidden" name="taskId" value={task.id} />
<button type="submit">×</button>
</fetcher.Form>
</li>
))}
</ul>
</div>
);
}
Optimistic UI (delete)
export async function loader () {
const tasks = await prisma.task.findMany();
return json({ tasks });
};
export const action: ActionFunction = async ({ request }) => {
const formData = await request.formData();
const taskId = formData.get('taskId');
if (taskId) {
await prisma.task.delete({ where: { id: taskId } });
return json({});
}
return json({});
};
export default function Tasks() {
const { tasks } = useLoaderData();
const fetcher = useFetcher();
const isDeleting = fetcher.state === 'submitting' && fetcher.formData?.get('taskId');
return (
<div>
<h1>Tasks</h1>
<ul>
{tasks.map(task => (
<li
key={task.id}
style={{ display:
isDeleting && fetcher.formData?.get('taskId') === task.id ?
'none' : 'block'
}}>
{task.name}
<fetcher.Form method="post">
<input type="hidden" name="taskId" value={task.id} />
<button type="submit">×</button>
</fetcher.Form>
</li>
))}
</ul>
</div>
);
}
Optimistic UI (delete)
export async function loader () {
const tasks = await prisma.task.findMany();
return json({ tasks });
};
export const action: ActionFunction = async ({ request }) => {
const formData = await request.formData();
const taskId = formData.get('taskId');
if (taskId) {
try {
await prisma.task.delete({ where: { id: taskId } });
return json({ ok: true });
} catch (e) {
return json({ error: true });
}
}
return json({ ok: true });
};
export default function Tasks() {
const { tasks } = useLoaderData();
const fetcher = useFetcher();
const isDeleting = fetcher.state === 'submitting' && fetcher.formData?.get('taskId');
const isFailedDeletion = fetcher.data?.error;
return (
<div>
<h1>Tasks</h1>
<ul>
{tasks.map(task => (
<li key={task.id} style={{ display: isDeleting && fetcher.formData?.get('taskId') === task.id ? 'none' : 'block' }}>
{task.name}
<fetcher.Form method="post">
<input type="hidden" name="taskId" value={task.id} />
<button type="submit" aria-label={isFailedDeletion ? "Retry" : "Delete"}>
{isFailedDeletion ? "Retry" : "×"}
</button>
</fetcher.Form>
</li>
))}
</ul>
</div>
);
}
Optimistic UI Add
export async function loader() {
const tasks = await prisma.task.findMany();
return json({ tasks });
}
export default function Tasks() {
const { tasks } = useLoaderData<typeof loader>();
return (
<div>
<h1>Tasks</h1>
{tasks.map((task) => <Task key={task.name} name={task.name} deadline={task.name} />)}
</div>
);
}
Optimistic UI Add
export async function loader() {
const tasks = await prisma.task.findMany();
return json({ tasks });
}
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const _action = formData.get('_action');
if (_action === 'add') {
try {
const taskName = formData.get('taskName') || '';
const deadline = formData.get('deadline') || '';
await prisma.task.create({
data: { name: taskName, deadline }
});
return json({ ok: true });
} catch (e) {
return json({ error: true });
}
}
return json({ ok: true });
}
export default function Tasks() {
const { tasks } = useLoaderData<typeof loader>();
const fetcher = useFetcher<typeof action>()
return (
<div>
<h1>Tasks</h1>
{tasks.map((task) => <Task key={task.name} name={task.name} deadline={task.name} />)}
<fetcher.Form method="post">
<input type="hidden" />
<input type="text" name="taskName" placeholder="Task Name" />
<input type="text" name="deadline" placeholder="Deadline" />
<button type="submit" name="_action" value="add">Add Task</button>
</fetcher.Form>
</div>
);
}
Optimistic UI Add
export async function loader() {
const tasks = await prisma.task.findMany();
return json({ tasks });
}
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const _action = formData.get('_action');
if (_action === 'add') {
try {
const taskName = formData.get('taskName') || '';
const deadline = formData.get('deadline') || '';
await prisma.task.create({
data: { name: taskName, deadline }
});
return json({ ok: true });
} catch (e) {
return json({ error: true });
}
}
return json({ ok: true });
}
export default function Tasks() {
const { tasks } = useLoaderData<typeof loader>();
const fetcher = useFetcher<typeof action>();
const navigation = useNavigation();
const isAdding = navigation.state === 'submitting' && navigation?.formData.get('_action') === 'add';
const optimisticTaskName = navigation?.formData.get('taskName')?.toString() || '';
const optimisticDeadline = navigation?.formData.get('deadline')?.toString() || '';
return (
<div>
<h1>Tasks</h1>
{tasks.map((task) => <Task key={task.name} name={task.name} deadline={task.name} />)}
{isAdding && (
<Task
name={optimisticTaskName}
deadline={optimisticDeadline}
isOptimistic
/>
)}
<fetcher.Form method="post">
<input type="hidden" />
<input type="text" name="taskName" placeholder="Task Name" />
<input type="text" name="deadline" placeholder="Deadline" />
<button type="submit" name="_action" value="add">Add Task</button>
</fetcher.Form>
</div>
);
}
Routing
Routing
app/
├── page.tsx
├── about/
│ └── page.tsx
├── concerts/
│ ├── page.tsx
│ ├── [city]/
│ │ └── page.tsx
│ ├── trending/
│ │ └── page.tsx
├── posts/
│ └── [...slug]/
│ └── page.tsx
├── (group)/
│ ├── page.tsx
│ ├── index/
│ │ └── page.tsx
│ ├── folder/
│ │ └── page.tsx
├── other/
│ └── page.tsx
└── layout.tsx app/
├── routes/
│ ├── _index/
│ │ └── route.tsx
│ ├── about/
│ │ └── route.tsx
│ ├── concerts/
│ │ ├── _index/
│ │ │ └── route.tsx
│ │ ├── $city/
│ │ │ └── route.tsx
│ │ ├── trending/
│ │ │ └── route.tsx
│ ├── posts/
│ │ └── $/
│ │ └── route.tsx
│ ├── _group/
│ │ ├── _index/
│ │ │ └── route.tsx
│ │ ├── folder/
│ │ │ └── route.tsx
│ └── other/
│ └── route.tsx
└── root.tsx
Routing
app/
├── page.tsx
├── about/
│ └── page.tsx
├── concerts/
│ ├── page.tsx
│ ├── [city]/
│ │ └── page.tsx
│ ├── trending/
│ │ └── page.tsx
├── posts/
│ └── [...slug]/
│ └── page.tsx
├── (group)/
│ ├── page.tsx
│ ├── index/
│ │ └── page.tsx
│ ├── folder/
│ │ └── page.tsx
├── other/
│ └── page.tsx
└── layout.tsx app/
├── routes/
│ ├── _index/
│ │ └── route.tsx
│ ├── about/
│ │ └── route.tsx
│ ├── concerts/
│ │ ├── _index/
│ │ │ └── route.tsx
│ │ ├── $city/
│ │ │ └── route.tsx
│ │ ├── trending/
│ │ │ └── route.tsx
│ ├── posts/
│ │ └── $/
│ │ └── route.tsx
│ ├── _group/
│ │ ├── _index/
│ │ │ └── route.tsx
│ │ ├── folder/
│ │ │ └── route.tsx
│ └── other/
│ └── route.tsx
└── root.tsx
Routing
app/
├── page.tsx
├── about/
│ └── page.tsx
├── concerts/
│ ├── page.tsx
│ ├── [city]/
│ │ └── page.tsx
│ ├── trending/
│ │ └── page.tsx
├── posts/
│ └── [...slug]/
│ └── page.tsx
├── (group)/
│ ├── page.tsx
│ ├── index/
│ │ └── page.tsx
│ ├── folder/
│ │ └── page.tsx
├── other/
│ └── page.tsx
└── layout.tsx app/
├── routes/
│ ├── _index/
│ │ └── route.tsx
│ ├── about/
│ │ └── route.tsx
│ ├── concerts/
│ │ ├── _index/
│ │ │ └── route.tsx
│ │ ├── $city/
│ │ │ └── route.tsx
│ │ ├── trending/
│ │ │ └── route.tsx
│ ├── posts/
│ │ └── $/
│ │ └── route.tsx
│ ├── _group/
│ │ ├── _index/
│ │ │ └── route.tsx
│ │ ├── folder/
│ │ │ └── route.tsx
│ └── other/
│ └── route.tsx
└── root.tsx
Routing
app/
├── page.tsx
├── about/
│ └── page.tsx
├── concerts/
│ ├── page.tsx
│ ├── [city]/
│ │ └── page.tsx
│ ├── trending/
│ │ └── page.tsx
├── posts/
│ └── [...slug]/
│ └── page.tsx
├── (group)/
│ ├── page.tsx
│ ├── index/
│ │ └── page.tsx
│ ├── folder/
│ │ └── page.tsx
├── other/
│ └── page.tsx
└── layout.tsx app/
├── routes/
│ ├── _index/
│ │ └── route.tsx
│ ├── about/
│ │ └── route.tsx
│ ├── concerts/
│ │ ├── _index/
│ │ │ └── route.tsx
│ │ ├── $city/
│ │ │ └── route.tsx
│ │ ├── trending/
│ │ │ └── route.tsx
│ ├── posts/
│ │ └── $/
│ │ └── route.tsx
│ ├── _group/
│ │ ├── _index/
│ │ │ └── route.tsx
│ │ ├── folder/
│ │ │ └── route.tsx
│ └── other/
│ └── route.tsx
└── root.tsx
Routing
app/
├── page.tsx
├── about/
│ └── page.tsx
├── concerts/
│ ├── page.tsx
│ ├── [city]/
│ │ └── page.tsx
│ ├── trending/
│ │ └── page.tsx
├── posts/
│ └── [...slug]/
│ └── page.tsx
├── (group)/
│ ├── page.tsx
│ ├── index/
│ │ └── page.tsx
│ ├── folder/
│ │ └── page.tsx
├── other/
│ └── page.tsx
└── layout.tsx app/
├── routes/
│ ├── _index/
│ │ └── route.tsx
│ ├── about/
│ │ └── route.tsx
│ ├── concerts/
│ │ ├── _index/
│ │ │ └── route.tsx
│ │ ├── $city/
│ │ │ └── route.tsx
│ │ ├── trending/
│ │ │ └── route.tsx
│ ├── posts/
│ │ └── $/
│ │ └── route.tsx
│ ├── _group/
│ │ ├── _index/
│ │ │ └── route.tsx
│ │ ├── folder/
│ │ │ └── route.tsx
│ └── other/
│ └── route.tsx
└── root.tsx
Routing
app/
├── page.tsx
├── about/
│ └── page.tsx
├── concerts/
│ ├── page.tsx
│ ├── [city]/
│ │ └── page.tsx
│ ├── trending/
│ │ └── page.tsx
├── posts/
│ └── [...slug]/
│ └── page.tsx
├── (group)/
│ ├── page.tsx
│ ├── index/
│ │ └── page.tsx
│ ├── folder/
│ │ └── page.tsx
├── other/
│ └── page.tsx
└── layout.tsx app/
├── routes/
│ ├── _index/
│ │ └── route.tsx
│ ├── about/
│ │ └── route.tsx
│ ├── concerts/
│ │ ├── _index/
│ │ │ └── route.tsx
│ │ ├── $city/
│ │ │ └── route.tsx
│ │ ├── trending/
│ │ │ └── route.tsx
│ ├── posts/
│ │ └── $/
│ │ └── route.tsx
│ ├── _group/
│ │ ├── _index/
│ │ │ └── route.tsx
│ │ ├── folder/
│ │ │ └── route.tsx
│ └── other/
│ └── route.tsx
└── root.tsx
Cool bits
- parallel fetching,
- managing concurrent connections,
- client side actions & loaders,
- error boundaries,
- streaming,
- pre-built session storages

Resources
Resources
Thank You!
Questions?
Remix
By Marcin Żmudka
Remix
- 22