Remix Crash Course: Building Full-Stack Applications
Introduction to Remix
Welcome to an educational journey into Remix, a modern, full-stack JavaScript framework designed for building robust and user-centric web applications. This chapter will guide you through the fundamental concepts of Remix, exploring its benefits and demonstrating its practical application in building a blog application.
Remix is not just another framework; it’s a deliberate approach to web development, created by the same minds behind React Router. It addresses many challenges inherent in traditional Single Page Applications (SPAs) by embracing server-side rendering and leveraging web fundamentals. While frameworks like Next.js also offer server-side rendering for React, Remix distinguishes itself with its unique data loading and form handling mechanisms, particularly through the use of loaders and actions.
Framework: In software development, a framework provides a structured environment and pre-built components to simplify and accelerate the development process. It offers a foundation upon which developers can build applications, reducing boilerplate code and promoting best practices.
This chapter will not delve into a comparative analysis between Remix and Next.js. Both are powerful frameworks, but Remix offers a distinct philosophy and approach to server-rendered React applications, which we will explore in detail. The industry trend is clearly shifting towards server-rendered applications, and Remix is at the forefront of this movement.
Why Remix? Benefits of a Full-Stack Framework
Remix offers a compelling set of advantages, particularly for React developers seeking to build performant and user-friendly web applications. Let’s explore some of the key benefits:
1. Server-Side Rendering (SSR)
Server-Side Rendering (SSR): A technique where the initial HTML of a web page is rendered on the server rather than in the user’s browser. This allows the browser to display content faster, improving initial load times and Search Engine Optimization (SEO).
Server-side rendering is a cornerstone of Remix’s architecture, providing significant benefits:
- Improved SEO: Search engines can easily crawl and index server-rendered content, boosting your website’s visibility in search results.
- Faster Initial Load Times: Users see content faster, leading to a better user experience, especially on slower networks or devices.
- Enhanced Performance: The server handles the initial rendering, reducing the workload on the client-side browser.
2. File System Routing
File System Routing: A routing mechanism where the structure of your application’s routes is determined by the file system directory structure. Creating a file within a specific directory automatically defines a route, eliminating the need for explicit route configuration.
Remix adopts file system routing, simplifying route management:
- Intuitive Route Definition: Routes are implicitly defined by the file structure within your
routes
directory. Creating a file automatically establishes a corresponding route. - Reduced Configuration: Developers don’t need to manually configure and maintain route definitions, streamlining the development process.
- Clearer Project Structure: The file system directly reflects the application’s routing structure, enhancing project organization and maintainability.
This approach is reminiscent of traditional server-side frameworks and offers a straightforward way to manage application navigation.
3. Nested Routes
Nested Routes: A routing pattern that allows for hierarchical route structures. Child routes are rendered within the context of their parent routes, enabling the creation of layouts and reusable UI components that are shared across multiple related routes.
Remix supports nested routes, enabling complex and organized application layouts:
- Hierarchical UI Structure: Build applications with nested layouts, where components like navigation bars or sidebars can be shared across multiple related pages.
- Code Reusability: Utilize React Router’s
Outlet
component to define placeholders where child route content will be rendered within a parent route’s layout. - Enhanced Organization: Structure routes logically, mirroring the application’s UI hierarchy and improving code organization.
Remix leverages React Router’s Outlet
component to facilitate nested routing, offering a hybrid approach that combines the benefits of SPAs and server-rendered applications.
React Router: A popular JavaScript library for declarative routing in React applications. It provides components and hooks for managing navigation and defining application routes.
4. Loaders and Actions: Core Remix Concepts
Loaders and Actions are central to Remix’s data handling and form submission approach, distinguishing it from traditional SPAs and even other server-rendered frameworks.
Loaders
Loaders: Functions defined within Remix route modules that execute on the server to fetch data required for rendering a specific route. Loaders run before the route component is rendered, ensuring data is available when the page loads.
Loaders provide a powerful mechanism for server-side data fetching:
- Server-Side Data Fetching: Loaders execute exclusively on the server, enabling secure and efficient data retrieval from databases, APIs, or any server-side data source.
- Data Availability on Page Load: Loaders ensure data is fetched and ready before the React component for a route is rendered, preventing loading spinners and improving user experience.
- Integration with Data Sources: Loaders can seamlessly integrate with various data sources, including databases (like SQLite using Prisma, as demonstrated in the transcript), APIs, and file systems.
In the context of the blog application, loaders will be used to fetch blog posts from the database and provide them to the React components for display.
Actions
Actions: Functions defined within Remix route modules that handle form submissions and mutations on the server. Actions are invoked when a form is submitted to a route, allowing server-side processing of form data and performing actions like database updates or user authentication.
Actions revolutionize form handling in web applications:
- Form Submission without JavaScript: Remix allows traditional HTML form submissions to be handled directly by server-side action functions, eliminating the need for client-side JavaScript form handling in many cases.
- Server-Side Form Processing: Actions execute on the server, providing a secure and reliable way to process form data, perform validations, and interact with databases.
- Simplified Form Handling: By leveraging actions, developers can simplify form logic and reduce the amount of client-side JavaScript required for form interactions.
Actions enable a more traditional, server-centric approach to form processing, reminiscent of frameworks like PHP, while leveraging the power of React for UI rendering.
5. Easy Access to Head Tags
Remix provides convenient ways to manage <head>
tags within route modules:
- Meta Tags and SEO: Easily add meta tags (like keywords and descriptions) to individual routes, improving SEO and providing contextual information to search engines and browsers.
- CSS and Link Management: Include route-specific CSS files or other links within the
<head>
section, ensuring proper styling and resource loading for each page.
6. Built-in Error Handling
Remix simplifies error management in web applications:
- Route-Specific Error Boundaries: Define
ErrorBoundary
components within route modules to handle errors that occur specifically within that route, providing localized error handling. - Root Error Boundary: Create a root
ErrorBoundary
to catch errors that occur outside of specific routes, providing a global error handling mechanism for the entire application.
Error Boundary: A React component that catches JavaScript errors anywhere in its child component tree, logs those errors, and displays a fallback UI instead of crashing the whole component tree.
7. TypeScript Support Out of the Box
TypeScript: A superset of JavaScript that adds optional static typing. TypeScript enhances code maintainability, readability, and helps catch errors during development.
Remix offers seamless TypeScript integration:
- TypeScript Boilerplate: Generate Remix applications with TypeScript support from the outset, leveraging the benefits of static typing.
- Gradual Adoption: Easily transition JavaScript files to TypeScript by simply changing the file extension to
.tsx
, allowing for gradual adoption of TypeScript within an existing project.
8. Built-in Support for Cookies and Sessions
Remix provides robust support for managing user sessions and cookies:
createCookie
Function: A utility function provided by Remix to simplify cookie creation and management.- Session Management: Built-in support for session handling using cookies, file system storage, server memory, or custom storage solutions.
Cookies: Small pieces of data that websites store on a user’s computer to remember information about the user, such as login status or preferences.
Sessions: A way to store information about a user across multiple requests, typically using cookies to identify the user’s session. Sessions are often used for user authentication and maintaining stateful interactions.
Building a Blog Application: A Practical Example
To solidify your understanding of Remix, we will embark on building a blog application. This hands-on approach will demonstrate the practical application of Remix concepts such as loaders, actions, and routing. We will also integrate Prisma, an Object-Relational Mapper (ORM), and SQLite, a lightweight database, to manage blog post data.
Object-Relational Mapper (ORM): A software layer that sits between an application and a relational database. ORMs allow developers to interact with databases using object-oriented programming concepts, abstracting away the complexities of raw SQL queries.
Prisma: A modern ORM for Node.js and TypeScript that simplifies database access and management. Prisma provides a type-safe and intuitive API for interacting with databases.
SQLite: A lightweight, file-based relational database engine. SQLite is self-contained, serverless, and requires no separate server process, making it ideal for development and smaller applications.
This blog application will feature basic functionalities including:
- Displaying a list of blog posts.
- Creating new blog posts.
- Viewing individual blog posts.
- Deleting blog posts.
- (Future Enhancement) User authentication (to be covered in a subsequent chapter).
Setting Up the Remix Application
Let’s begin by setting up a new Remix application. Open your terminal and follow these steps:
-
Create a new Remix project:
npx create-remix@latest remix-blog
You can choose to use
@latest
or specify a version like1.0.6
as mentioned in the transcript. -
Select deployment target: Choose “Remix App Server” for simplicity in this tutorial.
-
Choose TypeScript or JavaScript: For this tutorial, we will use JavaScript for broader accessibility. However, in production, TypeScript is highly recommended. Select “Just JavaScript”.
-
Run
npm install
: Answer “Yes” to runnpm install
to install project dependencies. -
Navigate to the project directory:
cd remix-blog
-
Open the project in your code editor: Use your preferred code editor (like Visual Studio Code).
-
Start the development server:
npm run dev
This command will start the Remix development server, typically accessible at
http://localhost:3000
. You should see a default Remix welcome page.
Exploring Core Remix Files and Concepts
Let’s examine the key files and folders within your newly created Remix application:
package.json
: Contains project dependencies, scripts (likedev
andbuild
), and other project metadata.public/
: A directory for static assets like images, fonts, and other files that are served directly to the browser.app/
: The heart of your Remix application. This directory contains:entry.client.jsx
: The client-side entry point of your application. This is the first JavaScript code that runs in the browser. It handles client-side rendering and hydration of React components.entry.server.jsx
: The server-side entry point. This code is executed on the server for every request. It’s responsible for handling requests, fetching data, rendering the initial HTML, and sending responses.root.jsx
: The root component of your application. It defines the basic HTML structure (<html>
,<head>
,<body>
) and includes essential components likeOutlet
for rendering routes andLiveReload
for development.routes/
: This directory houses your route modules. Each file within this directory defines a route in your application. Remix uses file system routing, so the file structure directly maps to your application’s URLs.styles/
: Contains global stylesheets and potentially route-specific stylesheets.utils/
: (To be created) A directory for utility functions, including database connection logic (db.server.ts
in the transcript, we will createdb.server.js
for JavaScript).
Setting up the Root Route (root.jsx
)
Let’s customize the root.jsx
file to establish the basic structure of our application. Replace the existing content of root.jsx
with the following:
import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration } from "remix";
import globalStylesUrl from "./styles/global.css";
export const links = () => {
return [{ rel: "stylesheet", href: globalStylesUrl }];
};
export default function App() {
return (
<Document>
<Layout>
<Outlet />
</Layout>
</Document>
);
}
function Document({ children, title }) {
return (
<html lang="en">
<head>
<Meta />
<Meta charSet="utf-8" />
<Meta name="viewport" content="width=device-width, initial-scale=1" />
<Title>{title ? title : "My Remix Blog"}</Title>
<Links />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}
function Layout({ children }) {
return (
<>
<nav className="navbar">
<Link to="/" className="logo">
Remix Blog
</Link>
<ul className="nav">
<li>
<Link to="/posts">Posts</Link>
</li>
</ul>
</nav>
<div className="container">{children}</div>
</>
);
}
function Title({ children }) {
return <title>{children}</title>;
}
function Link({ to, children, className }) {
return (
<a href={to} className={className}>
{children}
</a>
);
}
Explanation:
- Imports: Imports essential components from Remix, including
Links
,LiveReload
,Meta
,Outlet
,Scripts
, andScrollRestoration
. links
export: Exports alinks
function to include global stylesheets. In this case, it links toglobal.css
.App
Component: The main application component that renders theDocument
andLayout
components, withOutlet
acting as a placeholder for route-specific content.Document
Component: Defines the basic HTML document structure, including<head>
and<body>
tags. It includesMeta
,Title
,Links
,Scripts
,ScrollRestoration
, andLiveReload
components provided by Remix.Layout
Component: Creates a basic layout with a navigation bar containing a logo and a “Posts” link. It wraps theOutlet
within acontainer
div for styling.Title
andLink
Components: Simple helper components for rendering<title>
and<a>
tags.
Create global.css
:
Create a global.css
file inside the app/styles
directory and paste the CSS styles provided in the transcript or your own styles. This will style the basic layout and components.
Routing in Remix: Creating Routes for Blog Functionality
Now, let’s create the routes for our blog application within the routes
directory.
1. Home Page (routes/index.jsx
)
Create routes/index.jsx
and add the following content:
export default function Home() {
return (
<div>
<h1>Welcome to My Remix Blog</h1>
<p>This is a simple blog application built with Remix, Prisma, and SQLite.</p>
</div>
);
}
This will be the home page of your blog, accessible at /
.
2. Posts Route and Nested Routes (routes/posts.jsx
and routes/posts/
)
Create a folder named posts
inside the routes
directory.
Parent Posts Route (routes/posts.jsx
):
Create routes/posts.jsx
and add the following:
import { Outlet } from "remix";
export default function PostsRoute() {
return (
<>
<h1>Posts</h1>
<Outlet />
</>
);
}
This serves as the parent route for all post-related routes (like listing posts, creating new posts, viewing individual posts). The Outlet
component will render content from nested routes within this parent route.
Posts Index Route (routes/posts/index.jsx
):
Create routes/posts/index.jsx
and add the following:
export default function PostItems() {
return (
<div>
<h2>All Posts</h2>
{/* Post list will be rendered here */}
</div>
);
}
This will be the index route for posts, accessible at /posts
. We will later populate this with a list of blog posts fetched from the database using a loader.
New Post Route (routes/posts/new.jsx
):
Create routes/posts/new.jsx
and add the following:
import { Link } from "remix";
export default function NewPost() {
return (
<div>
<Link to="/posts" className="btn btn-reverse">
Back to Posts
</Link>
<h1>New Post</h1>
{/* Form for creating new posts will be added here */}
</div>
);
}
This route, accessible at /posts/new
, will contain the form for creating new blog posts.
Dynamic Post Route (routes/posts/$postId.jsx
):
Create routes/posts/$postId.jsx
and add the following:
import { useParams, Link } from "remix";
export default function Post() {
const params = useParams();
const postId = params.postId;
return (
<div>
<Link to="/posts" className="btn btn-reverse">
Back to Posts
</Link>
<h1>Post ID: {postId}</h1>
{/* Single post content will be displayed here */}
</div>
);
}
This is a dynamic route, accessible at URLs like /posts/123
, /posts/abc
. The $postId
part indicates a dynamic segment in the URL. useParams
hook is used to extract the postId
from the URL parameters.
Loaders and Data Fetching: Displaying Blog Posts
Now, let’s implement data fetching using loaders to display blog posts on the /posts
route.
Modify routes/posts/index.jsx
:
import { useLoaderData, Link } from "remix";
export async function loader() {
// Placeholder data - replace with database fetch later
const posts = [
{ id: 1, title: "Post 1", body: "This is a test post 1" },
{ id: 2, title: "Post 2", body: "This is a test post 2" },
{ id: 3, title: "Post 3", body: "This is a test post 3" },
];
return { posts };
}
export default function PostItems() {
const { posts } = useLoaderData();
return (
<div>
<div className="page-header">
<h1>Posts</h1>
<Link to="/posts/new" className="btn">
New Post
</Link>
</div>
<ul className="post-list">
{posts.map((post) => (
<li key={post.id}>
<Link to={`/posts/${post.id}`}>
<h3>{post.title}</h3>
</Link>
</li>
))}
</ul>
</div>
);
}
Explanation:
loader
function: An asynchronous function exported asloader
. This is the loader function for this route.- It currently returns hardcoded placeholder
posts
data. We will replace this with database fetching later. - It returns an object with
posts
property, which will be available to the component.
- It currently returns hardcoded placeholder
useLoaderData
hook: In thePostItems
component,useLoaderData
hook is used to access the data returned by theloader
function.- Rendering Post List: The component maps over the
posts
array and renders a list of posts, each with a link to its individual post page (/posts/${post.id}
).
Actions and Form Handling: Creating New Blog Posts
Let’s implement form handling using actions to allow users to create new blog posts on the /posts/new
route.
Modify routes/posts/new.jsx
:
import { ActionFunction, Form, Link, useActionData, redirect } from "remix";
export const action: ActionFunction = async ({ request }) => {
const formData = await request.formData();
const title = formData.get("title");
const body = formData.get("body");
// Placeholder - submit to database later
console.log("Form Data:", { title, body });
return redirect("/posts");
};
export default function NewPost() {
return (
<div>
<div className="page-header">
<Link to="/posts" className="btn btn-reverse">
Back to Posts
</Link>
<h1>New Post</h1>
</div>
<div className="page-content">
<Form method="post">
<div className="form-control">
<label htmlFor="title">Title</label>
<input type="text" name="title" id="title" />
</div>
<div className="form-control">
<label htmlFor="body">Post Body</label>
<textarea name="body" id="body" />
</div>
<button type="submit" className="btn btn-block">
Add Post
</button>
</Form>
</div>
</div>
);
}
Explanation:
action
function: An asynchronous function exported asaction
. This is the action function for this route, triggered on form submission.- It receives a
request
object containing information about the incoming request. request.formData()
is used to parse the form data submitted via the POST request.formData.get("title")
andformData.get("body")
extract the values of the “title” and “body” input fields from the form data.- Placeholder:
console.log
is used to temporarily display the form data. We will replace this with database interaction later. redirect("/posts")
redirects the user back to the/posts
route after form submission.
- It receives a
Form
component: TheForm
component from Remix is used to create the HTML form.method="post"
specifies that the form will be submitted using a POST request.name
attributes on input fields (name="title"
,name="body"
) are crucial for accessing form data in the action function.
Error Handling: Implementing Error Boundaries
Let’s implement error handling by adding an ErrorBoundary
component to the root.jsx
file.
Modify root.jsx
:
// ... (previous imports and components) ...
export function ErrorBoundary({ error }) {
console.error(error);
return (
<Document>
<Layout>
<div className="page-content">
<h1>Error</h1>
<p>Oops! Something went wrong.</p>
<pre>{error.message}</pre>
</div>
</Layout>
</Document>
);
}
Explanation:
ErrorBoundary
component: A function component exported asErrorBoundary
. This component will catch errors that occur during rendering of any route in your application.- It receives an
error
object containing details about the error. - It logs the error to the console (
console.error(error)
). - It renders a fallback UI with an error message and the error message from the
error
object.
- It receives an
Now, if an error occurs during rendering, Remix will catch it and render this ErrorBoundary
component, providing a user-friendly error message instead of a broken page.
Integrating Prisma and SQLite: Setting up the Database
Let’s integrate Prisma and SQLite into our Remix application to persist blog post data in a database.
-
Install Prisma CLI and Client:
npm install prisma @prisma/client
-
Initialize Prisma:
npx prisma init --datasource-provider sqlite
This command creates a
prisma
directory withschema.prisma
file and sets up SQLite as the database provider. -
Define the Prisma Schema (
prisma/schema.prisma
):Replace the contents of
prisma/schema.prisma
with the following:generator client { provider = "prisma-client-js" } datasource db { provider = "sqlite" url = "file:./dev.db" } model Post { id String @id @default(uuid()) title String body String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt }
Explanation:
generator client
: Configures the Prisma Client generation.datasource db
: Configures the database connection.provider = "sqlite"
specifies SQLite as the database.url = "file:./dev.db"
sets the database file todev.db
in theprisma
directory.
model Post
: Defines thePost
model, representing the blog post table in the database.id
: Unique identifier for each post (String, UUID, primary key).title
: Title of the post (String).body
: Content of the post (String).createdAt
: Timestamp of post creation (DateTime, automatically set on creation).updatedAt
: Timestamp of last post update (DateTime, automatically updated on update).
-
Push the schema to the database:
npx prisma db push
This command creates the
dev.db
SQLite database file and applies the schema defined inschema.prisma
. -
Create
db.server.js
(app/utils/db.server.js
):Create a
utils
directory insideapp
and then createdb.server.js
with the following content:import { PrismaClient } from "@prisma/client"; let prisma; if (process.env.NODE_ENV === "production") { prisma = new PrismaClient(); prisma.$connect(); } else { if (!global.__db) { global.__db = new PrismaClient(); global.__db.$connect(); } prisma = global.__db; } export default prisma;
Explanation:
- Import
PrismaClient
: Imports the Prisma Client constructor. - Global Prisma Instance: Creates a global
prisma
instance to reuse database connections in development and ensure a new connection in production. - Conditional Connection:
- Production: Creates a new
PrismaClient
and connects to the database directly. - Development: Uses a global variable (
global.__db
) to store the Prisma Client instance. If it doesn’t exist, it creates a new one and stores it globally. This prevents creating new database connections on every code change during development, improving performance.
- Production: Creates a new
- Export
prisma
: Exports theprisma
instance, making it available for use in other modules.
- Import
Database Seeding: Adding Initial Blog Posts
Let’s create a database seeder to populate our SQLite database with initial blog posts.
-
Create
prisma/seed.js
:Create a
seed.js
file inside theprisma
directory with the following content:const { PrismaClient } = require("@prisma/client"); const db = new PrismaClient(); async function getPosts() { return [ { title: "First Post", body: "This is the body of the first post.", }, { title: "Second Post", body: "This is the body of the second post.", }, { title: "Third Post", body: "This is the body of the third post.", }, { title: "Fourth Post", body: "This is the body of the fourth post.", }, ]; } async function seed() { await Promise.all( getPosts().map((post) => { return db.post.create({ data: post }); }) ); } seed() .catch((e) => { console.error(e); process.exit(1); }) .finally(async () => { await db.$disconnect(); });
Explanation:
- Import
PrismaClient
: ImportsPrismaClient
usingrequire
(CommonJS syntax). getPosts
function: Returns an array of sample post objects withtitle
andbody
.seed
function: An asynchronous function that:- Uses
Promise.all
andmap
to iterate over the posts fromgetPosts
. - For each post, it uses
db.post.create({ data: post })
to create a newPost
record in the database with the post data.
- Uses
- Run
seed
function: Calls theseed
function and handles potential errors and database disconnection.
- Import
-
Run the seeder:
node prisma/seed.js
This command executes the
seed.js
script, populating yourdev.db
database with the sample posts.
Fetching Data with Loaders (Database Integration)
Now, let’s modify the loader in routes/posts/index.jsx
to fetch blog posts from the SQLite database using Prisma.
Modify routes/posts/index.jsx
:
import { useLoaderData, Link } from "remix";
import prisma from "~/utils/db.server"; // Import Prisma client
export async function loader() {
const posts = await prisma.post.findMany({
take: 20, // Limit to 20 posts
select: {
id: true,
title: true,
createdAt: true,
},
orderBy: {
createdAt: "desc", // Order by creation date descending
},
});
return { posts };
}
// ... (PostItems component remains the same) ...
Explanation:
- Import
prisma
: Imports the Prisma Client instance from~/utils/db.server.js
. - Database Query in
loader
:await prisma.post.findMany(...)
uses Prisma Client to fetch multiplePost
records from the database.take: 20
: Limits the number of fetched posts to 20.select: { ... }
: Specifies which fields to retrieve (id, title, createdAt) for optimization, as we only need these for the post list.orderBy: { createdAt: "desc" }
: Orders the posts bycreatedAt
in descending order (newest first).
Now, when you navigate to /posts
, the list of blog posts will be dynamically fetched from your SQLite database.
Creating New Posts with Actions (Database Integration)
Let’s update the action in routes/posts/new.jsx
to persist new blog posts to the database.
Modify routes/posts/new.jsx
:
import { ActionFunction, Form, Link, redirect } from "remix";
import prisma from "~/utils/db.server"; // Import Prisma client
export const action: ActionFunction = async ({ request }) => {
const formData = await request.formData();
const title = formData.get("title");
const body = formData.get("body");
if (!title || typeof title !== "string" || title.length === 0) {
return { errors: { title: "Title is required" } }; // Basic validation
}
if (!body || typeof body !== "string" || body.length === 0) {
return { errors: { body: "Body is required" } }; // Basic validation
}
const post = await prisma.post.create({
data: {
title,
body,
},
});
return redirect(`/posts/${post.id}`);
};
export default function NewPost() {
// ... (rest of the component, you can add useActionData for error display if needed) ...
Explanation:
- Import
prisma
: Imports the Prisma Client instance. - Database
create
Operation inaction
:await prisma.post.create({ data: { title, body } })
uses Prisma Client to create a newPost
record in the database with thetitle
andbody
extracted from the form data.- Basic validation is added to check if title and body are present. You can expand this validation further.
- Redirect to Post Page:
redirect(
/posts/${post.id})
redirects the user to the newly created post’s individual page after successful creation.
Now, when you submit the form on /posts/new
, a new blog post will be created in your SQLite database, and you will be redirected to the post’s page.
Displaying Single Posts
Let’s modify the loader in routes/posts/$postId.jsx
to fetch and display a single blog post from the database.
Modify routes/posts/$postId.jsx
:
import { useLoaderData, useParams, Link } from "remix";
import prisma from "~/utils/db.server"; // Import Prisma client
export async function loader({ params }) {
const postId = params.postId;
const post = await prisma.post.findUnique({
where: {
id: postId,
},
});
if (!post) {
throw new Error("Post not found"); // Error handling if post not found
}
return { post };
}
export default function Post() {
const { post } = useLoaderData();
return (
<div>
<div className="page-header">
<Link to="/posts" className="btn btn-reverse">
Back to Posts
</Link>
<h1>{post.title}</h1>
</div>
<div className="page-content">
<p>{post.body}</p>
</div>
</div>
);
}
Explanation:
- Import
prisma
: Imports the Prisma Client instance. - Database
findUnique
Query inloader
:await prisma.post.findUnique({ where: { id: postId } })
uses Prisma Client to fetch a singlePost
record from the database based on thepostId
URL parameter.- Error handling is added: If
post
is not found (null), it throws an error, which will be caught by theErrorBoundary
.
- Displaying Post Data: The
Post
component now usesuseLoaderData
to access the fetchedpost
data and displays the post’stitle
andbody
.
Now, when you navigate to URLs like /posts/123
, you will see the content of the corresponding blog post fetched from the database.
Deleting Posts
Finally, let’s add the functionality to delete blog posts on the single post page (routes/posts/$postId.jsx
).
Modify routes/posts/$postId.jsx
:
import { useLoaderData, useParams, Link, Form, ActionFunction, redirect } from "remix";
import prisma from "~/utils/db.server"; // Import Prisma client
export const action: ActionFunction = async ({ request, params }) => {
const formData = await request.formData();
const method = formData.get("_method");
if (method === "delete") {
const postId = params.postId;
await prisma.post.delete({
where: {
id: postId,
},
});
return redirect("/posts");
}
return null; // Handle other actions if needed
};
// ... (loader function remains the same) ...
export default function Post() {
const { post } = useLoaderData();
return (
<div>
<div className="page-header">
<Link to="/posts" className="btn btn-reverse">
Back to Posts
</Link>
<h1>{post.title}</h1>
</div>
<div className="page-content">
<p>{post.body}</p>
</div>
<div className="page-footer">
<Form method="post">
<input type="hidden" name="_method" value="delete" />
<button type="submit" className="btn btn-delete">
Delete Post
</button>
</Form>
</div>
</div>
);
}
Explanation:
action
function: An action function is added to handle the delete request.- It extracts the
_method
field from the form data. - If
_method
is “delete”, it proceeds with the delete operation. await prisma.post.delete({ where: { id: postId } })
uses Prisma Client to delete thePost
record with the matchingpostId
.redirect("/posts")
redirects the user back to the posts list after deletion.
- It extracts the
- Delete Form in
Post
Component:- A
Form
withmethod="post"
is added to thepage-footer
. <input type="hidden" name="_method" value="delete" />
adds a hidden input field named_method
with the value “delete”. This is a common Remix pattern to simulate DELETE requests using HTML forms, as HTML forms natively only support GET and POST methods.
- A
Now, you have a delete button on each post page. Submitting this form will trigger the action
function, delete the post from the database, and redirect you back to the posts list.
Conclusion
Congratulations! You have successfully built a basic blog application using Remix, Prisma, and SQLite. This chapter covered a wide range of Remix concepts, including:
- Introduction to Remix and its benefits.
- File system routing and nested routes.
- Loaders for server-side data fetching.
- Actions for handling form submissions.
- Error handling with Error Boundaries.
- Integration with Prisma ORM and SQLite database.
Remix offers a compelling approach to full-stack web development, combining the power of React with server-side rendering and a streamlined development experience. This chapter serves as a foundation for further exploration of Remix and its advanced features. In future chapters, we can expand this blog application with features like user authentication, post editing, and more complex UI interactions, further demonstrating the capabilities of Remix.