A brand-new React framework has come into the IT hub — Remix.run. It’s a wonderful framework with exclusive and next-gen features.
There are many React frameworks but Remix stands out, which is not surprising considering its powerful functionalities.
Remix can write both the backend and the frontend code in the same file. With minimal JavaScript, we can now render data on the server and to the client.
Unlike vanilla React, where we fetch data on the frontend, which is then rendered on the screen, Remix fetches data on the backend and serves the HTML to the user directly. This guide will give you an overview of Remix and show you how to get started with this powerful framework -
Remix.run is a newly developed, open-source and a wonderful full-stack web development framework designed to help you quickly build web applications with a minimum of hassle.
With Remix, you can easily create complex user interfaces with a user-friendly syntax. The framework is designed to be modular and extensible, so you can pick and choose the components you need for your project and create custom applications.
In short -
It’s wise to say that Remix is a complete pleasure to work with and is on the path of becoming one of the greatest frameworks.
The framework is still in its early stages, but it has already seen some success, with a number of high-profile projects using Remix.run to power their websites.
The first thing you need to make sure before trying Remix is to check whether you have all the prerequisites fulfilled, i.e.-
npx create-remix@latest --template remix-run/indie-stack blog-tutorial
You can find the detailed guide to create your own Remix app is given here, which if you follow step wise, you will see how easy it is. The most wonderful thing is stacks which you can use to start quickly with your app without having to create it from the scratch.
Also, a stack comes with a complete signup and login pages with UI and authentication.
Now, let’s check out the code of my first remix app, which is a simple blog-tutorial app, which i created with the above command. It shows some simple real-time examples of how routing works in remix.
Honestly, I followed the step-wise guide from the docs and created some pages through routing and nested routing. But the amazing thing was that the data was coming from Prisma, which is one of the greatest strengths of TypeScript.
Check out the result here-
Now, let’s check out the code- The code structure is same as given in the developer’s guide to Remix.
Here’s my app/routes.index.tsx
file for the home page-
import { Link } from "@remix-run/react";
import { useOptionalUser } from "~/utils";
export default function Index() {
const user = useOptionalUser();
return (
<main className="relative min-h-screen bg-white sm:flex sm:items-center sm:justify-center">
<div className="relative sm:pb-16 sm:pt-8">
<div className="mx-auto max-w-7xl sm:px-6 lg:px-8">
<div className="relative shadow-xl sm:overflow-hidden sm:rounded-2xl">
<div className="relative px-4 pt-16 pb-8 sm:px-6 sm:pt-24 sm:pb-14 lg:px-8 lg:pb-20 lg:pt-32">
<h1 className="text-center text-5xl font-extrabold tracking-tight sm:text-4xl lg:text-9xl">
<span className="block uppercase text-yellow-500 drop-shadow-md">
Indie Stack
</span>
</h1>
<div className="mx-auto mt-16 max-w-7xl text-center">
<Link
to="/posts"
className="text-xl text-blue-600 underline"
>
Click here to check Blog Posts
</Link>
</div>
<div className="mx-auto mt-10 max-w-sm sm:flex sm:max-w-none sm:justify-center">
{user ? (
<Link
to="/notes"
className="flex items-center justify-center rounded-md border border-transparent bg-white px-4 py-3 text-base font-medium text-yellow-700 shadow-sm hover:bg-yellow-50 sm:px-8"
>
View Notes for {user.email}
</Link>
) : (
<div className="space-y-4 sm:mx-auto sm:inline-grid sm:grid-cols-2 sm:gap-5 sm:space-y-0">
<Link
to="/join"
className="flex items-center justify-center rounded-md border border-transparent bg-white px-4 py-3 text-base font-medium text-yellow-700 shadow-sm hover:bg-yellow-50 sm:px-8"
>
Sign up
</Link>
<Link
to="/login"
className="flex items-center justify-center rounded-md bg-yellow-500 px-4 py-3 font-medium text-white hover:bg-yellow-600"
>
Log In
</Link>
</div>
)}
</div>
<a href="https://remix.run">
<img
src="https://user-images.githubusercontent.com/1500684/158298926-e45dafff-3544-4b69-96d6-d3bcc33fc76a.svg"
alt="Remix"
className="mx-auto mt-16 w-full max-w-[12rem] md:max-w-[16rem]"
/>
</a>
</div>
</div>
</div>
</div>
</main>
);
}
This is the app/models/post.server.ts
file where we define our post models-
import { prisma } from "~/db.server";
import type { Post } from "@prisma/client";
export type { Post };
export async function getPosts() {
return prisma.post.findMany();
}
export async function getPost(slug: string) {
return prisma.post.findUnique({ where: { slug } });
}
export async function createPost(
post: Pick<Post, "slug" | "title" | "markdown">
) {
return prisma.post.create({ data: post });
}
}
Here’s the app/routes/posts/index.tsx
file for the /posts route page-
import { json } from "@remix-run/node";
import { Link, useLoaderData } from "@remix-run/react";
import { getPosts } from "~/models/post.server";
type LoaderData = {
// this is a handy way to say: "posts is whatever type getPosts resolves to"
posts: Awaited<ReturnType<typeof getPosts>>;
};
export const loader = async () => {
return json<LoaderData>({
posts: await getPosts(),
});
};
export default function Posts() {
const { posts } = useLoaderData() as LoaderData;
return (
<main>
<div className="absolute inset-0">
<div className="absolute inset-0 bg-[color:rgba(00,204,27,0.5)] mix-blend-multiply" />
</div>
<div className="relative px-4 pt-16 pb-8 sm:px-6 sm:pb-14 lg:px-8 lg:pb-20">
<h1 className="text-center pb-8 text-3xl font-bold underline tracking-tight sm:text-6xl ">Posts</h1>
<Link to="admin" className="text-red-600 underline">
Admin
</Link>
<ul>
{posts.map((post) => (
<li key={post.slug}>
<Link
to={post.slug}
className="flex text-violet-600 underline"
>
{post.title}
</Link>
</li>
))}
</ul>
</div>
</main>
);
}
In the above file where we are getting the posts data, we have used the exported loader function which contains the loaded posts through useLoaderData hook. This is an excellent example that shows how easily remix combines the frontend and the backend.
The prisma schema file, i.e. prisma/schema.prisma
looks like this-
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id String @id @default(cuid())
email String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
password Password?
notes Note[]
}
model Password {
hash String
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
userId String @unique
}
model Note {
id String @id @default(cuid())
title String
body String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
userId String
}
model Post {
slug String @id
title String
markdown String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
This is the seed.ts
file for prisma-
import { PrismaClient } from "@prisma/client";
import bcrypt from "bcryptjs";
const prisma = new PrismaClient();
async function seed() {
const email = "rachel@remix.run";
// cleanup the existing database
await prisma.user.delete({ where: { email } }).catch(() => {
// no worries if it doesn't exist yet
});
const hashedPassword = await bcrypt.hash("racheliscool", 10);
const user = await prisma.user.create({
data: {
email,
password: {
create: {
hash: hashedPassword,
},
},
},
});
await prisma.note.create({
data: {
title: "My first note",
body: "Hello, world!",
userId: user.id,
},
});
await prisma.note.create({
data: {
title: "My second note",
body: "Hello, world!",
userId: user.id,
},
});
const posts = [
{
slug: "my-first-post",
title: "My First Post",
markdown: `
# This is my first post
Isn't it great?
`.trim(),
},
{
slug: "90s-mixtape",
title: "A Mixtape I Made Just For You",
markdown: `
# 90s Mixtape
- I wish (Skee-Lo)
- This Is How We Do It (Montell Jordan)
- Everlong (Foo Fighters)
- Ms. Jackson (Outkast)
- Interstate Love Song (Stone Temple Pilots)
- Killing Me Softly With His Song (Fugees, Ms. Lauryn Hill)
- Just a Friend (Biz Markie)
- The Man Who Sold The World (Nirvana)
- Semi-Charmed Life (Third Eye Blind)
- ...Baby One More Time (Britney Spears)
- Better Man (Pearl Jam)
- It's All Coming Back to Me Now (Céline Dion)
- This Kiss (Faith Hill)
- Fly Away (Lenny Kravits)
- Scar Tissue (Red Hot Chili Peppers)
- Santa Monica (Everclear)
- C'mon N' Ride it (Quad City DJ's)
`.trim(),
},
];
for (const post of posts) {
await prisma.post.upsert({
where: { slug: post.slug },
update: post,
create: post,
});
}
console.log(`Database has been seeded. 🌱`);
}
seed()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
Here goes app/routes/posts/$slug.tsx
file to get post slugs-
import { marked } from "marked";
import type { LoaderFunction } from "@remix-run/node";
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import invariant from "tiny-invariant";
import type { Post } from "~/models/post.server";
import { getPost } from "~/models/post.server";
type LoaderData = { post: Post; html: string };
export const loader: LoaderFunction = async ({
params,
}) => {
invariant(params.slug, `params.slug is required`);
const post = await getPost(params.slug);
invariant(post, `Post not found: ${params.slug}`);
const html = marked(post.markdown);
return json<LoaderData>({ post, html });
};
export default function PostSlug() {
const { post, html } = useLoaderData() as unknown as LoaderData;
return (
<main className="mx-auto max-w-4xl">
<h1 className="my-6 border-b-2 text-center text-3xl">
{post.title}
</h1>
<div dangerouslySetInnerHTML={{ __html: html }} />
</main>
);
}
Here is the app/routes/posts/admin.tsx
file for our /admin page-
import type { LoaderFunction } from "@remix-run/node";
import { json } from "@remix-run/node";
import {
Link,
Outlet,
useLoaderData,
} from "@remix-run/react";
import { getPosts } from "~/models/post.server";
type LoaderData = {
posts: Awaited<ReturnType<typeof getPosts>>;
};
export const loader: LoaderFunction = async () => {
return json({ posts: await getPosts() });
};
export default function PostAdmin() {
const { posts } = useLoaderData() as LoaderData;
return (
<div className="mx-auto max-w-4xl">
<h1 className="my-6 mb-2 border-b-2 text-center text-3xl">
Blog Admin
</h1>
<div className="grid grid-cols-4 gap-6">
<nav className="col-span-4 md:col-span-1">
<ul>
{posts.map((post) => (
<li key={post.slug}>
<Link
to={post.slug}
className="text-blue-600 underline"
>
{post.title}
</Link>
</li>
))}
</ul>
</nav>
<main className="col-span-4 md:col-span-3">
<Outlet />
</main>
</div>
</div>
);
}
Check the app/routes/posts/admin/index.tsx
file where we link to /new route -
import { Link } from "@remix-run/react";
export default function AdminIndex() {
return (
<p>
<Link to="new" className="text-blue-600 underline">
Create a New Post
</Link>
</p>
);
}
Finally, the file app/routes/posts/admin/new.tsx
, which is a nested route component, where we create a form, validate it and use it to create a new post -
import { Form, useActionData, useTransition } from "@remix-run/react";
import type { ActionFunction } from "@remix-run/node";
import { redirect, json } from "@remix-run/server-runtime";
import invariant from "tiny-invariant";
import { createPost } from "~/models/post.server";
const inputClassName = `w-full rounded border border-gray-500 px-2 py-1 text-lg`;
type ActionData =
| {
title: null | string;
slug: null | string;
markdown: null | string;
}
| undefined;
export const action: ActionFunction = async ({
request,
}) => {
const formData = await request.formData();
const title = formData.get("title");
const slug = formData.get("slug");
const markdown = formData.get("markdown");
const errors: ActionData = {
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<ActionData>(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");
};
export default function NewPost() {
const errors = useActionData();
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"
className="rounded bg-blue-500 py-2 px-4 text-white hover:bg-blue-600 focus:bg-blue-400 disabled:bg-blue-300"
disabled={isCreating}
>
{isCreating ? "Creating..." : "Create Post"}
</button>
</p>
</Form>
);
}
Yes, you can create a form using <Form> tag, which is same as the <form> tag in HTML, and understand the form submission. Additionally, the action function here is responsible for getting and processing the form data.
And, that’s it folks! We did it. We created our first app with Remix, you can also update your UI through tailwind CSS.
Now, let’s catch up on some amazing things that Remix has introduced:
A stack is a container that holds all the necessary components to run an application, which makes them much smaller and faster. Remix.run comes with several built-in and official stacks that are in itself an application, including -
In short, everything from the backend to the frontend, database and required dependencies.
This way, you can just choose a stack and get started with your project quickly, enhancing the code in your own way and that too, without having to care about the setup and configuration.
Amazing right?
Here are the built-in stacks that Remix provides -
One of the key features of Remix.run is its routing system, which allows you to define the structure of your URLs and makes it easy to map URLs to specific resources in your application.
In Remix.run, routes can be nested within other routes, using params, to create more complex applications. Nested routing allows you to break up your application into smaller, more manageable pieces. It also makes it easier to reuse code and keep your application organized.
To create a nested route, you simply need to add a new route object within an existing route. The new route will then be accessible at the path specified by its parent route.
For example, if you have a route named /users and you add a nested route named /profile, the /profile route will be accessible at /users/profile. In the code examples, the /new is the nested route within parent /admin route, as we can see here.
When we click on the ‘Create a New Post’ link as given here-
It opens up the route within the existing one, like this-
As we saw above, the loader function eliminates the need of creating APIs to call data through getPosts controller, or creating backend controller to handle the GET API, and other files for routing and managing the code.
It’s a huge development in Remix, which is yet to get noticed by the world. There’s no separate files for frontend and backend code, you can write both in a single file, which is completely opposite to the concept of ‘Separation of concerns’.
Though this was already happening with the introduction of JSX in React, but Remix is taking it to a whole other level.
You are writing the whole page in a single file along with its css and database request, which is a big step that brings a sense of unity of frontend and backend within the code and eases the code flow.
Besides all the wonderful things we learned here about Remix, the one that still stands out is that somehow, among working together with the backend as well as the frontend code, Remix has made development easier and NOT complicated.
Once you get the concept right, it’s plain and simple HTML.