Remix
| Created | |
|---|---|
| Type | Stack |
| Language | Javascript |
| Last Edit |
Tutorial

Basics
Description
Remix is an edge native, full stack JavaScript framework for building modern, fast, and resilient user experiences. It unifies the client and server with web standards
Prerequisites
- Node.js version (^14.17.0, or >=16.0.0)
- npm 7 or greater
Setup
Empty Project
npx create-remix@latest <project-name>- The Blues Stack: Deployed to the edge (distributed) with a long-running Node.js server and PostgreSQL database. Intended for large and fast production-grade applications serving millions of users.
- The Indie Stack: Deployed to a long-running Node.js server with a persistent SQLite database. This stack is great for websites with dynamic data that you control (blogs, marketing, content sites). It's also a perfect, low-complexity bootstrap for MVPs, prototypes, and proof-of-concepts that can later be updated to the Blues stack easily.
- The Grunge Stack: Deployed to a serverless function running Node.js with DynamoDB for persistence. Intended for folks who want to deploy a production-grade application on AWS infrastructure serving millions of users.
Template Project
npx create-remix@latest --template remix-run/indie-stack blog-tutorialRun
npm run devRoutes
Each page in Remix will be rendered through routes.
Create
touch app/routes/posts/index.ts
export default function Posts() {
return (
<main>
<h1>Posts</h1>
</main>
);
}Link To Created Route
import { Link } from "@remix-run/react";
const Index = () => {
<Link to="/posts">
Blog Posts
</Link>
}Dynamic Routes
Example Routes
/posts/my-first-post
/posts/90s-mixtapeAccess Params
import type { LoaderArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import invariant from "tiny-invariant";
import { getPost } from "~/models/post.server";
export const loader = async ({ params }: LoaderArgs) => {
invariant(params.slug, `params.slug is required`);
const post = await getPost(params.slug);
invariant(post, `Post not found: ${params.slug}`);
return json({ post });
};
export default function PostSlug() {
const { post } = useLoaderData<typeof loader>();
return (
<main>
<h1>{post.title}</h1>
</main>
);
}
The part of the filename attached to the $ becomes a named key on the params object that comes into your loader.
Here file is named as $slug.txt
Invariant
For validation.
Validation required by TypeScript.
Because params comes from the URL, not totally sure that params.slug will be defined : Also happens when file name is changed.
export async function getPost(slug: string) {
return prisma.post.findUnique({ where: { slug } });
}Nested Routing
Relative Routing
The link will route to /posts/admin even though link to is just admin
<Link to="admin">
Admin
</Link>Index Routes
import { json } from "@remix-run/node";
import {
Link,
Outlet,
useLoaderData,
} from "@remix-run/react";
import { getPosts } from "~/models/post.server";
export const loader = async () => {
return json({ posts: await getPosts() });
};
export default function PostAdmin() {
const { posts } = useLoaderData<typeof loader>();
return (
<div>
<h1>
Blog Admin
</h1>
<div>
<na>
<ul>
{posts.map((post) => (
<li key={post.slug}>
<Link
to={post.slug}
>
{post.title}
</Link>
</li>
))}
</ul>
</nav>
<main>
<Outlet />
</main>
</div>
</div>
);
}
Every route inside of app/routes/posts/admin/ can now render inside of app/routes/posts/admin.tsx when their URL matches.
You get to control which part of the admin.tsx layout the child routes render.
Now the admin page will render the list of posts and create new post link.
import { Link } from "@remix-run/react";
export default function AdminIndex() {
return (
<p>
<Link to="new">
Create a New Post
</Link>
</p>
);
}export default function NewPost() {
return <h2>New Post</h2>;
}
Once Create a New Post link is clicked, Remix outlet automatically swaps index.tsx to new.tsx
This is because when the URL matches the parent route’s path, the index will render inside the outlet.
Data
Load Data
import { json } from "@remix-run/node";
import { Link, useLoaderData } from "@remix-run/react";
import { getPosts } from "~/models/post.server";
export const loader = async () => {
return json({
posts: await getPosts(),
});
};
export default function Posts() {
const { posts } = useLoaderData<typeof loader>();
console.log(posts);
return (
<main>
<h1>Posts</h1>
<ul>
{posts.map((post) => (
<li key={post.slug}>
<Link to={post.slug} className="text-blue-600 underline">
{post.title}
</Link>
</li>
))}
</ul>
</main>
);
}
import { prisma } from "~/db.server";
export async function getPosts() {
return prisma.post.findMany();
}
Send Data
Form
import type { Post } from "@prisma/client";
export async function createPost(
post: Pick<Post, "slug" | "title" | "markdown">
) {
return prisma.post.create({ data: post });
}import { Form, useActionData, useTransition } from "@remix-run/react";
import type { ActionArgs } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import invariant from "tiny-invariant";
import { createPost } from "~/models/post.server";
export const action = async ({ request }: ActionArgs) => {
// TODO: remove me
await new Promise((res) => setTimeout(res, 1000));
const formData = await request.formData();
const title = formData.get("title");
const slug = formData.get("slug");
const markdown = formData.get("markdown");
const errors = {
title: title ? null : "Title is required",
slug: slug ? null : "Slug is required",
markdown: markdown ? null : "Markdown is required",
};
const hasErrors = Object.values(errors).some((errorMessage) => errorMessage);
if (hasErrors) {
return json(errors);
}
invariant(typeof title === "string", "title must be a string");
invariant(typeof slug === "string", "slug must be a string");
invariant(typeof markdown === "string", "markdown must be a string");
await createPost({ title, slug, markdown });
return redirect("/posts/admin");
};
const inputClassName = `w-full rounded border border-gray-500 px-2 py-1 text-lg`;
export default function NewPost() {
const errors = useActionData<typeof action>();
// Loading UI
const transition = useTransition();
const isCreating = Boolean(transition.submission);
return (
<Form method="post">
<p>
<label>
Post Title:{" "}
{errors?.title ? (
<em className="text-red-600">{errors.title}</em>
) : null}
<input type="text" name="title" className={inputClassName} />
</label>
</p>
<p>
<label>
Post Slug:{" "}
{errors?.slug ? (
<em className="text-red-600">{errors.slug}</em>
) : null}
<input type="text" name="slug" className={inputClassName} />
</label>
</p>
<p>
<label htmlFor="markdown">
Markdown:{" "}
{errors?.markdown ? (
<em className="text-red-600">{errors.markdown}</em>
) : null}
</label>
<br />
<textarea
id="markdown"
rows={20}
name="markdown"
className={`${inputClassName} font-mono`}
/>
</p>
<p className="text-right">
<button
type="submit"
disabled={isCreating}
className="rounded bg-blue-500 py-2 px-4 text-white hover:bg-blue-600 focus:bg-blue-400 disabled:bg-blue-300"
>
{isCreating ? "Creating..." : "Create Post"}
</button>
</p>
</Form>
);
}
Update Data
export async function updatePost(
slug: string,
post: Pick<Post, "title" | "markdown">
) {
return prisma.post.update({ data: post, where: { slug } });
}Show default values that is fetched using useLoaderData.
Form require key so that whenever key changes, form is rerendered. If no key is provided, then the form won’t be updated with the new values.
import {
Form,
useActionData,
useLoaderData,
useTransition,
} from "@remix-run/react";
import invariant from "tiny-invariant";
import type { LoaderArgs, ActionArgs } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import { updatePost, getPost } from "~/models/post.server";
export const action = async ({ request }: ActionArgs) => {
// TODO: remove me
await new Promise((res) => setTimeout(res, 1000));
const formData = await request.formData();
const title = formData.get("title");
const slug = formData.get("slug");
const markdown = formData.get("markdown");
const errors = {
title: title ? null : "Title is required",
slug: slug ? null : "Slug is required",
markdown: markdown ? null : "Markdown is required",
};
const hasErrors = Object.values(errors).some((errorMessage) => errorMessage);
if (hasErrors) {
return json(errors);
}
invariant(typeof title === "string", "title must be a string");
invariant(typeof slug === "string", "slug must be a string");
invariant(typeof markdown === "string", "markdown must be a string");
await updatePost(slug, { title, markdown });
return redirect("/posts/admin");
};
const inputClassName = `w-full rounded border border-gray-500 px-2 py-1 text-lg`;
export const loader = async ({ params }: LoaderArgs) => {
invariant(params.slug, `params.slug is required`);
const post = await getPost(params.slug);
invariant(post, `Post not found: ${params.slug}`);
return json({ post });
};
export default function PostSlug() {
const { post } = useLoaderData<typeof loader>();
const errors = useActionData<typeof action>();
const transition = useTransition();
const isUpdating = Boolean(transition.submission);
return (
<Form method="post" key={post.title}>
<p>
<label>
Post Title:{" "}
{errors?.title ? (
<em className="text-red-600">{errors.title}</em>
) : null}
<input
defaultValue={post.title}
type="text"
name="title"
className={inputClassName}
/>
</label>
</p>
<p>
<label htmlFor="markdown">
Markdown:{" "}
{errors?.markdown ? (
<em className="text-red-600">{errors.markdown}</em>
) : null}
</label>
<br />
<textarea
id="markdown"
defaultValue={post.markdown}
rows={20}
name="markdown"
className={`${inputClassName} font-mono`}
/>
</p>
<p className="text-right">
<button
type="submit"
disabled={isUpdating}
className="rounded bg-blue-500 py-2 px-4 text-white hover:bg-blue-600 focus:bg-blue-400 disabled:bg-blue-300"
>
{isUpdating ? "Updating..." : "Update Post"}
</button>
</p>
</Form>
);
}