Remix

Text

  • What is Remix?
  • How Remix works under the hood?
  • Data Flow
  • Mutations
  • Routing
  • 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 }) => {
  let formData = new URLSearchParams(await request.text());
  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 }) => {
  let formData = new URLSearchParams(await request.text());
  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 }) => {
  let formData = new URLSearchParams(await request.text());
  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

Resources

Resources

Made with Slides.com