YouTube Courses - Learn Smarter

YouTube-Courses.site transforms YouTube videos and playlists into structured courses, making learning more efficient and organized.

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:

  1. Create a new Remix project:

    npx create-remix@latest remix-blog

    You can choose to use @latest or specify a version like 1.0.6 as mentioned in the transcript.

  2. Select deployment target: Choose “Remix App Server” for simplicity in this tutorial.

  3. Choose TypeScript or JavaScript: For this tutorial, we will use JavaScript for broader accessibility. However, in production, TypeScript is highly recommended. Select “Just JavaScript”.

  4. Run npm install: Answer “Yes” to run npm install to install project dependencies.

  5. Navigate to the project directory:

    cd remix-blog
  6. Open the project in your code editor: Use your preferred code editor (like Visual Studio Code).

  7. 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 (like dev and build), 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 like Outlet for rendering routes and LiveReload 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 create db.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, and ScrollRestoration.
  • links export: Exports a links function to include global stylesheets. In this case, it links to global.css.
  • App Component: The main application component that renders the Document and Layout components, with Outlet acting as a placeholder for route-specific content.
  • Document Component: Defines the basic HTML document structure, including <head> and <body> tags. It includes Meta, Title, Links, Scripts, ScrollRestoration, and LiveReload components provided by Remix.
  • Layout Component: Creates a basic layout with a navigation bar containing a logo and a “Posts” link. It wraps the Outlet within a container div for styling.
  • Title and Link 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 as loader. 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.
  • useLoaderData hook: In the PostItems component, useLoaderData hook is used to access the data returned by the loader 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 as action. 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") and formData.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.
  • Form component: The Form 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 as ErrorBoundary. 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.

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.

  1. Install Prisma CLI and Client:

    npm install prisma @prisma/client
  2. Initialize Prisma:

    npx prisma init --datasource-provider sqlite

    This command creates a prisma directory with schema.prisma file and sets up SQLite as the database provider.

  3. 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 to dev.db in the prisma directory.
    • model Post: Defines the Post 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).
  4. Push the schema to the database:

    npx prisma db push

    This command creates the dev.db SQLite database file and applies the schema defined in schema.prisma.

  5. Create db.server.js (app/utils/db.server.js):

    Create a utils directory inside app and then create db.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.
    • Export prisma: Exports the prisma instance, making it available for use in other modules.

Database Seeding: Adding Initial Blog Posts

Let’s create a database seeder to populate our SQLite database with initial blog posts.

  1. Create prisma/seed.js:

    Create a seed.js file inside the prisma 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: Imports PrismaClient using require (CommonJS syntax).
    • getPosts function: Returns an array of sample post objects with title and body.
    • seed function: An asynchronous function that:
      • Uses Promise.all and map to iterate over the posts from getPosts.
      • For each post, it uses db.post.create({ data: post }) to create a new Post record in the database with the post data.
    • Run seed function: Calls the seed function and handles potential errors and database disconnection.
  2. Run the seeder:

    node prisma/seed.js

    This command executes the seed.js script, populating your dev.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 multiple Post 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 by createdAt 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 in action:
    • await prisma.post.create({ data: { title, body } }) uses Prisma Client to create a new Post record in the database with the title and body 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 in loader:
    • await prisma.post.findUnique({ where: { id: postId } }) uses Prisma Client to fetch a single Post record from the database based on the postId URL parameter.
    • Error handling is added: If post is not found (null), it throws an error, which will be caught by the ErrorBoundary.
  • Displaying Post Data: The Post component now uses useLoaderData to access the fetched post data and displays the post’s title and body.

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 the Post record with the matching postId.
    • redirect("/posts") redirects the user back to the posts list after deletion.
  • Delete Form in Post Component:
    • A Form with method="post" is added to the page-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.

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.