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
routesdirectory. 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
Outletcomponent 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
ErrorBoundarycomponents within route modules to handle errors that occur specifically within that route, providing localized error handling. - Root Error Boundary: Create a root
ErrorBoundaryto 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:
createCookieFunction: 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-blogYou can choose to use
@latestor specify a version like1.0.6as 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 installto 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 devThis 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 (likedevandbuild), 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 likeOutletfor rendering routes andLiveReloadfor 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.tsin the transcript, we will createdb.server.jsfor 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. linksexport: Exports alinksfunction to include global stylesheets. In this case, it links toglobal.css.AppComponent: The main application component that renders theDocumentandLayoutcomponents, withOutletacting as a placeholder for route-specific content.DocumentComponent: Defines the basic HTML document structure, including<head>and<body>tags. It includesMeta,Title,Links,Scripts,ScrollRestoration, andLiveReloadcomponents provided by Remix.LayoutComponent: Creates a basic layout with a navigation bar containing a logo and a “Posts” link. It wraps theOutletwithin acontainerdiv for styling.TitleandLinkComponents: 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:
loaderfunction: An asynchronous function exported asloader. This is the loader function for this route.- It currently returns hardcoded placeholder
postsdata. We will replace this with database fetching later. - It returns an object with
postsproperty, which will be available to the component.
- It currently returns hardcoded placeholder
useLoaderDatahook: In thePostItemscomponent,useLoaderDatahook is used to access the data returned by theloaderfunction.- Rendering Post List: The component maps over the
postsarray 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:
actionfunction: An asynchronous function exported asaction. This is the action function for this route, triggered on form submission.- It receives a
requestobject 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.logis used to temporarily display the form data. We will replace this with database interaction later. redirect("/posts")redirects the user back to the/postsroute after form submission.
- It receives a
Formcomponent: TheFormcomponent from Remix is used to create the HTML form.method="post"specifies that the form will be submitted using a POST request.nameattributes 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:
ErrorBoundarycomponent: A function component exported asErrorBoundary. This component will catch errors that occur during rendering of any route in your application.- It receives an
errorobject 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
errorobject.
- 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 sqliteThis command creates a
prismadirectory withschema.prismafile and sets up SQLite as the database provider. -
Define the Prisma Schema (
prisma/schema.prisma):Replace the contents of
prisma/schema.prismawith 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.dbin theprismadirectory.
model Post: Defines thePostmodel, 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 pushThis command creates the
dev.dbSQLite database file and applies the schema defined inschema.prisma. -
Create
db.server.js(app/utils/db.server.js):Create a
utilsdirectory insideappand then createdb.server.jswith 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
prismainstance to reuse database connections in development and ensure a new connection in production. - Conditional Connection:
- Production: Creates a new
PrismaClientand 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 theprismainstance, 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.jsfile inside theprismadirectory 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: ImportsPrismaClientusingrequire(CommonJS syntax). getPostsfunction: Returns an array of sample post objects withtitleandbody.seedfunction: An asynchronous function that:- Uses
Promise.allandmapto iterate over the posts fromgetPosts. - For each post, it uses
db.post.create({ data: post })to create a newPostrecord in the database with the post data.
- Uses
- Run
seedfunction: Calls theseedfunction and handles potential errors and database disconnection.
- Import
-
Run the seeder:
node prisma/seed.jsThis command executes the
seed.jsscript, populating yourdev.dbdatabase 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 multiplePostrecords 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 bycreatedAtin 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
createOperation inaction:await prisma.post.create({ data: { title, body } })uses Prisma Client to create a newPostrecord in the database with thetitleandbodyextracted 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
findUniqueQuery inloader:await prisma.post.findUnique({ where: { id: postId } })uses Prisma Client to fetch a singlePostrecord from the database based on thepostIdURL parameter.- Error handling is added: If
postis not found (null), it throws an error, which will be caught by theErrorBoundary.
- Displaying Post Data: The
Postcomponent now usesuseLoaderDatato access the fetchedpostdata and displays the post’stitleandbody.
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:
actionfunction: An action function is added to handle the delete request.- It extracts the
_methodfield from the form data. - If
_methodis “delete”, it proceeds with the delete operation. await prisma.post.delete({ where: { id: postId } })uses Prisma Client to delete thePostrecord with the matchingpostId.redirect("/posts")redirects the user back to the posts list after deletion.
- It extracts the
- Delete Form in
PostComponent:- A
Formwithmethod="post"is added to thepage-footer. <input type="hidden" name="_method" value="delete" />adds a hidden input field named_methodwith 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.