This guide is part two of a three-part series on building a modern single-page application with Ruby on Rails and Inertia.js. In this article, we’ll dive into implementing CRUD functionality.

Part 1:
We set up a Rails-Inertia-React environment with basic routing, responses, and hot reloading to streamline frontend development.

Part 3: We’ll explore additional features, including reviewing Inertia requests, implementing nested routing, generating JS routes, and adding serializers to refine and complete our app.

Source code: https://github.com/greybutton/rails_inertia_blog

Implementing CRUD

1. Setup Models and Controllers

Stop the app from the previous step Ctrl+C. This section covers the setup for an Article and Comment model, along with their associated controllers and migrations.

# app/controllers/web/application_controller.rb
class Web::ApplicationController < ApplicationController
end

# app/controllers/web/articles_controller.rb
class Web::ArticlesController < Web::ApplicationController
end

Web::ApplicationController: A base controller under the Web namespace.
Web::ArticlesController: Handles article-related actions under the web/articles route.

Routes: Add a namespace for web, with resources for articles:


# config/routes.rb
namespace :web do
  resources :articles
end

Generate Article Model. This command creates an Article model with title and body fields:


docker compose run --rm web bash -c "rails generate model Article title:string body:text"

Generate Comment Model. Create a Comment model with fields for commenter and body, as well as a reference to article:


docker compose run --rm web bash -c "rails generate model Comment commenter:string body:text article:references"

Apply the migrations to create the articles and comments tables:


docker compose run --rm web bash -c "rails db:migrate"

Add validations and associations to the Article model to ensure each article has a title and body, and that the body has a minimum length of 10 characters. Update app/models/article.rb as follows:

class Article < ApplicationRecord
  has_many :comments

  validates :title, presence: true
  validates :body, presence: true, length: { minimum: 10 }
end

Keep app/models/comment.rb unchanged, as it already has the necessary association.

Use the following commands to start a shell session in the web container, open the Rails console, and create a sample Article record:

# Start a shell in the web container
docker compose run --rm web bash

# Open Rails console
rails console

# Create a sample article
Article.create(title: "Hello Rails", body: "I am on Rails!")

# Exit the rails console
exit

# Exit the web container
exit

This completes the Setup Models and Controllers section. Your Rails app now includes models with validations, associations, and a basic test for creating records in the Rails console.

2. Showing a List of Articles

Here’s how to display a simple list of articles. In this setup, we’re just rendering the titles of each article. We’ll expand on this with full CRUD actions in the following sections and update the list view accordingly.

1. Update the ArticlesController

Add an index action in Web::ArticlesController to fetch all articles and render them using Inertia:

# app/controllers/web/articles_controller.rb
class Web::ArticlesController < Web::ApplicationController
  def index
    @articles = Article.all

    render inertia: "articles/Index", props: {
      articles: @articles,
    }
  end
end

2. Create the Index Component for Articles

This React component displays the list of articles, using each article’s title as a list item:

// app/javascript/packs/web/articles/Index.jsx
import React from "react";

const Articles = ({ articles }) => (
  <>
    <h1>Articles</h1>
    <ul>
      {articles.map((article) => (
        <li key={article.id}>{article.title}</li>
      ))}
    </ul>
  </>
);

export default Articles;

Start the application and open http://localhost:3000/web/articles to see the list of articles.

At this point, you should see a simple list of articles displayed on the page.

3 Showing a Single Article

To make each article in the list clickable, we’ll add a link using web_article_path, which generates URLs in the format /web/articles/$ID. This allows users to click on an article and view its details.

1. Update ArticlesController to Add Link in Props

Modify the index action to include web_article_path for each article. Additionally, add a show action to handle displaying individual articles:

# app/controllers/web/articles_controller.rb
class Web::ArticlesController < Web::ApplicationController
  def index
    @articles = Article.all

    render inertia: "articles/Index", props: {
      articles: @articles.map do |article|
        article.attributes.merge(
          web_article_path: web_article_path(article),
        )
      end,
    }
  end

  def show
    @article = Article.find(params[:id])

    render inertia: "articles/Show", props: {
      article: @article,
    }
  end
end

Here’s why we do this:

  • Purpose: By adding web_article_path directly to each article in the @articles array, we make it easy to pass a pre-built link to each article directly from the server.
  • Flexibility: This approach allows the front-end to simply use web_article_path without needing to construct URLs manually. This keeps the front-end code simpler and focused only on rendering, while the Rails backend handles URL generation.
  • Consistency: By using Rails path helpers like web_article_path, we maintain consistency with the Rails routing system, ensuring that if routes change in the future, they only need to be updated in one place.

2. Update the Index Component to Add Links

In the Index component, wrap each article title in a component from Inertia. This will enable navigation without a full page reload:

// app/javascript/packs/web/articles/Index.jsx
import React from "react";
import { Link } from "@inertiajs/react";

const Articles = ({ articles }) => (
  <>
    <h1>Articles</h1>
    <ul>
      {articles.map((article) => (
        <li key={article.id}>
          <Link href={article.web_article_path}>{article.title}</Link>
        </li>
      ))}
    </ul>
  </>
);

export default Articles;

3. Create the Show Component for Article Details

The Show component renders the title and body of a single article:

// app/javascript/packs/web/articles/Show.jsx
import React from "react";

const Article = ({ article }) => (
  <>
    <h1>{article.title}</h1>
    <p>{article.body}</p>
  </>
);

export default Article;

Open http://localhost:3000/web/articles to view the list of articles. Clicking on an article will navigate to its detail page, for example, http://localhost:3000/web/articles/1.

4 Creating a New Article

In this section, we’ll add the ability to create a new article. Here’s how to set up the new and create actions and a form for submitting new articles.

1. Update ArticlesController

new action: Initializes a new article and renders the `New` component.
create action: Attempts to save the article and redirects back to the article list if successful or re-renders the form with errors if unsuccessful.

# app/controllers/web/articles_controller.rb
class Web::ArticlesController < Web::ApplicationController
  def index
    @articles = Article.all

    render inertia: "articles/Index", props: {
      articles: @articles.map do |article|
        article.attributes.merge(
          web_article_path: web_article_path(article),
        )
      end,
      new_web_article_path: new_web_article_path,
    }
  end

  def new
    @article = Article.new

    render inertia: "articles/New", props: {
      article: @article,
      web_articles_path: web_articles_path,
    }
  end

  def create
    @article = Article.new(article_params)

    if @article.save
      redirect_to web_articles_path
    else
      redirect_to new_web_article_path, inertia: { errors: @article.errors }
    end
  end

  private

  def article_params
    params.require(:article).permit(:title, :body)
  end
end

This addition passes the new_web_article_path to the front end, allowing a link to the “New Article” page without hardcoding the path in JavaScript. Just like with web_article_path, this makes routing dynamic and centralized, keeping URL generation within Rails and reducing the chance of inconsistencies.

2. Update the Index Component to Add a “New Article” Link

// app/javascript/packs/web/articles/Index.jsx
import React from "react";
import { Link } from "@inertiajs/react";

const Articles = ({ articles, new_web_article_path }) => (
  <>
    <h1>Articles</h1>
    <ul>
      {articles.map((article) => (
        <li key={article.id}>
          <Link href={article.web_article_path}>{article.title}</Link>
        </li>
      ))}
    </ul>
    <Link href={new_web_article_path}>New Article</Link>
  </>
);

export default Articles;

3. Create the New Component for Adding Articles

The New component renders the article form. The Form component is reused to handle article creation.

// app/javascript/packs/web/articles/New.jsx
import React from "react";
import { Link } from "@inertiajs/react";

import Form from "./_Form";

const New = ({ article, web_articles_path }) => {
  const handleSubmit = (form) => {
    form.post(web_articles_path);
  };

  return (
    <>
      <h1>New Article</h1>
      <Form
        article={article}
        onSubmit={handleSubmit}
        submitText="Create Article"
      />
      <Link href={web_articles_path}>List Articles</Link>
    </>
  );
};

export default New;

4. Create the _Form Component for Article Submission

// app/javascript/packs/web/articles/_Form.jsx
import React from "react";
import { useForm } from "@inertiajs/react";

const Form = ({ article, onSubmit, submitText }) => {
  const form = useForm({
    title: article.title || "",
    body: article.body || "",
  });
  const { data, setData, processing, errors } = form;

  const handleSubmit = (e) => {
    e.preventDefault();
    onSubmit(form);
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          type="text"
          value={data.title}
          onChange={(e) => setData("title", e.target.value)}
        />
        {errors.title && <div>{errors.title}</div>}
      </div>
      <div>
        <input
          type="textarea"
          value={data.body}
          onChange={(e) => setData("body", e.target.value)}
        />
        {errors.body && <div>{errors.body}</div>}
      </div>
      <button type="submit" disabled={processing}>
        {submitText}
      </button>
    </form>
  );
};

export default Form;

– Open http://localhost:3000/web/articles
– Click “New Article” to open the form.
– Try creating an article with a short body (fewer than 10 characters). You’ll see a validation error, showing that the Article model requires the body to meet the minimum length requirement.
– Once you correct the errors, you can successfully create a new article and view it in the list.

This setup ensures that URLs and validation errors are generated consistently, and it keeps the logic for routing and validation primarily within Rails, simplifying front-end code and making future updates easier.

5 Updating an Article

This section covers the setup for editing an article. We’ll add the edit and update actions in the controller, an edit form, and a link to navigate to the edit page.

1. Update ArticlesController

show action: Add edit_web_article_path to the props to provide a link to the edit page.
edit action: Initializes the article for editing and renders the Edit component.
update action: Attempts to update the article with submitted parameters, redirects to the article page if successful, or re-renders the form with errors.

# app/controllers/web/articles_controller.rb
class Web::ArticlesController < Web::ApplicationController
  # existing actions...

  def show
    @article = Article.find(params[:id])

    render inertia: "articles/Show", props: {
      article: @article,
      edit_web_article_path: edit_web_article_path(@article),
    }
  end

  def edit
    @article = Article.find(params[:id])

    render inertia: "articles/Edit", props: {
      article: @article,
      web_article_path: web_article_path,
    }
  end

  def update
    @article = Article.find(params[:id])

    if @article.update(article_params)
      redirect_to web_article_path(@article)
    else
      redirect_to edit_web_article_path, inertia: { errors: @article.errors }
    end
  end

  private

  def article_params
    params.require(:article).permit(:title, :body)
  end
end

2. Edit Component for Editing Articles

The Edit component displays a form for updating an article, reusing the _Form component to handle data entry and submission.

// app/javascript/packs/web/articles/Edit.jsx
import React from "react";

import Form from "./_Form";

const Edit = ({ article, web_article_path }) => {
  const handleSubmit = (form) => {
    form.put(web_article_path);
  };

  return (
    <>
      <h1>Edit Article</h1>
      <Form
        article={article}
        onSubmit={handleSubmit}
        submitText="Update Article"
      />
    </>
  );
};

export default Edit;

3. Show Component – Add Edit Link

Update the Show component to display an “Edit Article” link, allowing users to navigate to the edit form.

// app/javascript/packs/web/articles/Show.jsx
import React from "react";
import { Link } from "@inertiajs/react";

const Article = ({ article, edit_web_article_path }) => (
  <>
    <h1>{article.title}</h1>
    <p>{article.body}</p>
    <ul>
      <li>
        <Link href={edit_web_article_path}>Edit Article</Link>
      </li>
    </ul>
  </>
);

export default Article;

– Navigate to an article’s page, e.g., http://localhost:3000/web/articles/1
– Click “Edit Article” to open the edit form.
– Modify the article details and submit. You’ll be redirected to the updated article’s page if the update is successful.

This setup allows users to edit articles, with form validation handled both server-side and displayed via Inertia’s error handling.

6 Deleting an Article

In this section, we add functionality to delete an article from the list. This includes adding a destroy action in the controller and updating the Show component with a delete button.

1. Update ArticlesController

– In the show action, add web_article_path to the props to pass the path for the delete action.
– Add the destroy action to find the article by id, delete it, and redirect back to the list of articles.

# app/controllers/web/articles_controller.rb
class Web::ArticlesController < Web::ApplicationController
  # existing actions...

  def show
    @article = Article.find(params[:id])

    render inertia: "articles/Show", props: {
      article: @article,
      edit_web_article_path: edit_web_article_path(@article),
      web_article_path: web_article_path,
    }
  end

  def destroy
    @article = Article.find(params[:id])
    @article.destroy

    redirect_to web_articles_path, status: :see_other
  end

  private

  def article_params
    params.require(:article).permit(:title, :body)
  end
end

2. Update the Show Component to Add a Delete Button

In the Show component, we add a delete button that uses Inertia’s method="delete" attribute to send a delete request. The button includes a confirmation dialog to prevent accidental deletions. handleDestroy function: This function triggers a confirmation dialog. If the user cancels, it prevents the deletion by calling e.preventDefault().

// app/javascript/packs/web/articles/Show.jsx
import React from "react";
import { Link } from "@inertiajs/react";

const Article = ({ article, edit_web_article_path, web_article_path }) => {
  const handleDestroy = (e) => {
    if (!confirm("Are you sure?")) {
      e.preventDefault();
    }
  };

  return (
    <>
      <h1>{article.title}</h1>
      <p>{article.body}</p>
      <ul>
        <li>
          <Link href={edit_web_article_path}>Edit Article</Link>
        </li>
        <li>
          <Link
            href={web_article_path}
            onClick={handleDestroy}
            method="delete"
            as="button"
            type="button"
          >
            Delete Article
          </Link>
        </li>
      </ul>
    </>
  );
};

export default Article;

– Navigate to an article’s page, e.g., http://localhost:3000/web/articles/1
– Click “Delete Article”. You’ll see a confirmation dialog; selecting OK will delete the article and redirect you to the article list page.

This addition completes the CRUD functionality, allowing users to delete articles and the see_other (303) status ensures a smooth redirect after the deletion.

In this article, we built out CRUD functionality, enabling data interactions between Rails and Inertia. We covered setting up forms, handling validations, and ensuring smooth data flow for a responsive single-page experience.

In the next article, we’ll dive deeper into Inertia requests, nested routing and actions, and improvements like generating JS routes and adding serializers. These enhancements will simplify routing and give us control over the data we send to the frontend.