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
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"
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"
articles
and comments
tables:
docker compose run --rm web bash -c "rails db:migrate"
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.