Rails and Inertia.js - The Modern Monolith
Published: 2021-07-13 18:14:47

Rails is a powerful framework that gives us out of the box, tools like **Hotwire**, this helps us to build reactive applications, and also, we have external tools like **Stimulus Reflex** which like Hotwire, offer interactions that user is looking for (*you can see [this post](https://codeando.dev/posts/reactivity-rails-reflex/) about reactive applications with Reflex*).

But sometimes, you are looking for something else, maybe things that, tools like **React** or **Vue** can give you, can be that feeling of a SPA (Single Page Application). Rails has libraries like **Webpacker** that help us to use modern JS frameworks in an easy way but after installation, you need to do a lot of other things to get them to work in your applications, for example, you need to create some API's and then call them from JS or in some cases, is necessary to use client-side routing and it feels like you have two applications even when you have a monolith.

Here, is where **Inertia.js** comes to the action! 🎉

## What's Inertia.js?

It's a JS library that allows us to build a SPA (Single Page Applications) using the classic server-side render, I mean, using the views render, routing, and controllers as we would commonly do from the server-side without using API's or client-side routing, it only changes the way we render views, instead of returning HTML, views here are JavaScript Page Components.

> At this moment, Inertia just have three official client-side adapters (*React, Vue.js, and Svelte*) and two server-side adapters (*Laravel and Rails*).

## How does it work?

If you want to create a SPA, it's not enough to have your views with JS, because if you click in a link to navigate to another page, your browser would do a full reload page so this is not a behavior of a SPA 😞

Inertia, actually, is a client-side routing library that intercepts your clicks 😧 using its own anchor link wrapper called `<inertia-link>`, when you click on this element, *Inertia intercepts this event and makes the visit via `XHR` (XMLHttpRequest) instead*.

So, when Inertia makes an XHR visit, the server can detect that it's an Inertia request and instead of returning HTML, it returns a JSON response with the JavaScript page component name and data, finally, Inertia just replace this new component into the page and updates the history state.

### How does the server know if it's an Inertia request?

When someone makes the first request to your application, it's a full reload page and it returns HTML, there aren't any special headers at this point.

The previous HTML response serves the root `div` as a mounting point for the JS side, and this root contains the `data-page` attribute with a JSON page object.

> Inertia uses this page object to share data between client and server, this object contains these properties:
  > 1. Component: The name of the JavaScript page component.
  > 2. Props
  > 3. URL: The page URL
  > 4. Version: The current asset version (if there's asset changes, Inertia will automatically make a full page visit instead of an XHR visit).

The next requests are made via XHR with the `X-Inertia` header set to `true` (using the Inertia element anchor), this means that this request is through Inertia so, this the way that server knows if return just HTML or JS Page Components.

Next section will do an example with Rails and Vue.js! 🤟🏼

## Let's code!

### Creating the Rails project

When we create a new project, it's necessary *skip turbolinks* because it's not compatible with Inertia, but if you have a project with it and you want to add Inertia, you can disable Turbolinks for any response sent to Inertia, for example `redirect_to root_path, turbolinks: false`, for now, this the command to create the project:

```bash
rails new inertia_example --skip-turbolinks
```

### Installing Vue and Inertia

We could have installed Vue from the beginning like this:

```bash
rails new inertia_example --webpacker=vue --skip-turbolinks
```

But I want to show you how we can do it after installation in case anyone has a project already started, you can install Vue in this way:

```bash
rails webpacker:install:vue
```

Once Vue is installed, we need to add Inertia with this command:

```bash
yarn add @inertiajs/inertia @inertiajs/inertia-vue @inertiajs/progress
```

With this, we are installing *Inertia*, the *Inertia Vue adapter* and a *progress bar*, Inertia has an optional progress bar, so if you don't want to install it, just remove it from the command.

Next step is to install Inertia to the server-side running this command in the terminal:

```bash
bundle add 'inertia_rails'
```

This will install the latest version of `inertia_rails` gem, and finally it's necessary add `defer: true` to the `javascript_pack_tag` into the `app/views/layouts/application.html.erb` page:

```erb
<%= javascript_pack_tag 'application', defer: true %>
```

Adding `defer` we are telling to the `application.js` that will be executed after the page has been parsed, avoiding render errors with the `dataset`.

### Setting up

After Vue installation, we can delete these files `app/javascript/packs/hello_vue.js` and `app/javascript/app.vue` because we don't need them for now and let's initialize Vue inside `app/javascript/packs/application.js`

```js
// app/javascript/packs/application.js

...
import Vue from 'vue'
import { App, plugin } from '@inertiajs/inertia-vue'
import { InertiaProgress } from '@inertiajs/progress'

Vue.use(plugin)
InertiaProgress.init()

// Instead of using App.vue page, Inertia
// will use Rails application.html.erb layout page
const el = document.getElementById('app')

new Vue({
  render: h => h(App, {
    props: {
      initialPage: JSON.parse(el.dataset.page),
      resolveComponent: name => require(`../pages/${name}`).default,
    },
  }),
}).$mount(el)
```

You don't need to add the ID `app` to the root element, the Inertia adapter will do it for you 👍, `initialPage` gets the page where the response from the controller will be inserted, this page is a data attribute inside the root element and `resolveComponent` will look at the `pages` directory for the views, this is a common convention if you ever used Vue or Nuxt.

### Adding Tailwindcss

I'm going to use the [tailwindcss-rails](https://github.com/rails/tailwindcss-rails) gem, the only thing you need to do is to run these two commands in the console:

```bash
bundle add tailwindcss-rails
rails tailwindcss:install
```

After that, you already have Tailwindcss running in your project, you can also remove or comment the `stylesheet_link_tag` from your `app/views/layouts/application.html.erb`.

### Creating the Home page with Inertia

Let's create the `HomeController` here `app/controllers/home_controller.rb` with this 👇🏼

```ruby
class HomeController < ApplicationController
  def index
    render inertia: 'Home', props: {}
  end
end
```

We are just telling it that we're going to render a view called `Home` placed into `app/javascript/pages` without any props, just the view, remember the folder `pages` was defined in our `application.js` in `Setting up` section.

Next step is to add the root in the `config/routes.rb` file:

```ruby
Rails.application.routes.draw do
  root 'home#index'
end
```

Finally, we create the folder `pages` within `app/javascript` dir and we're going to add the `Home.vue` file

```vue
<!-- app/javascript/pages/Home.vue -->

<template>
  <div>
    <h1 class="text-2xl text-center">Home</h1>
    <h2 class="text-xl text-center">This is a Vue page rendered with Inertiajs</h2>
  </div>
</template>
```

If you prefer, you can add these classes to the `body` element in `application.html.erb`:

```erb
<!-- app/views/layouts/application.html.erb -->

...
<body class="p-5 bg-gray-100 mx-auto">
  <%= yield %>
</body>
```

Run your Rails server with `rails s` and to see the changes without reloading the page manually, just run `./bin/webpack-dev-server` in other console tab to watch the frontend changes, if you go to the browser, you'll see something like this:

![Home Page](https://res.cloudinary.com/codeando/image/upload/v1616645225/RailsInertiajs/Screen_Shot_2021-03-24_at_22.06.13_hmnwiu.png)

You got it! 🎉 Your first Vue page rendered with **Rails and Inertiajs**!

### Passing data to views

Now, we want to pass some data to Vue, for this, let's create a model called `Book` 👇🏼

```bash
rails g model Book title author edition editorial
...

rails db:migrate
```

We have our model with some attributes and it's time to create the *Books Controller*:

```ruby
# app/controllers/books_controller.rb

class BooksController < ApplicationController
  def index
    books = Book.all

    render inertia: 'Books/Index', props: {
      books: books.as_json(only: [:id, :title, :author, :edition, :editorial])
    }
  end
end
```

`Books/Index` means that we have a folder `Books` within `app/javascript/pages` and an `Index.vue` inside of and we are passing it a `books` array with the attributes we defined previously, so let's create the `Books/Index.vue` file:

```vue
<!-- app/javascript/pages/Books/Index.vue -->

<template>
  <div>
    <h1 class="text-2xl text-center">Books</h1>
    <table class="min-w-max w-full table-auto">
      <thead>
        <tr class="bg-gray-200 text-gray-600 uppercase text-sm leading-normal">
          <th class="py-3 px-6 text-left">Title</th>
          <th class="py-3 px-6 text-left">Author</th>
          <th class="py-3 px-6 text-center">Edition</th>
          <th class="py-3 px-6 text-center">Editorial</th>
          <th class="py-3 px-6 text-center">Actions</th>
        </tr>
      </thead>
      <tbody class="text-gray-600 text-sm font-light">
        <tr v-for="book in books" :key="book.id" class="border-b border-gray-200 hover:bg-gray-100">
          <td class="py-3 px-6 text-left"><span class="font-medium">{% raw %}{{ book.title }}{% endraw %}</span></td>
          <td class="py-3 px-6 text-left"><span class="font-medium">{% raw %}{{ book.author }}{% endraw %}</span></td>
          <td class="py-3 px-6 text-left"><span class="font-medium">{% raw %}{{ book.edition }}{% endraw %}</span></td>
          <td class="py-3 px-6 text-left"><span class="font-medium">{% raw %}{{ book.editorial }}{% endraw %}</span></td>
          <td class="py-3 px-6 text-left">
            <inertia-link :href="$routes.book(book.id)">See</inertia-link>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script>
  export default {
    props: {
      books: {
        type: Array,
        required: true,
      }
    }  
  }
</script>
```

And don't miss to add the routes for our Books resource:

```ruby
# config/routes.rb

Rails.application.routes.draw do
  root 'home#index'
  resources :books, only: %i[index show]
end
```

Before of going to the browser, I want to add some example data with the purpose of see something when we render the books page, you can add this code to `db/seeds.rb` and run `rails db:seed` to save those data into database

```ruby
10.times do |i|
  Book.create(
    title: "Metaprogramming Ruby Version-#{i}",
    author: "Paolo Perrotta",
    edition: "Edition #{i}",
    editorial: "The Pragmatic Programmers",
  )
end
```

So, go to `http://localhost:3000/books` and you should see something like this:

![Books page](https://res.cloudinary.com/codeando/image/upload/v1616648800/RailsInertiajs/Screen_Shot_2021-03-24_at_23.06.25_i4kkvk.png)

Great! 🤟🏼 **We are passing data from Rails to Vue using Inertia!**

### Inertia Links

Before using the `<inertia-link>` element, go to `books_controller.rb` and create the show method to get a book:

```ruby
def show
  book = Book.find(params[:id])

  render inertia: 'Books/Show', props: {
    book: book.as_json(only: [:id, :title, :author, :edition, :editorial])
  }
end
```

Now, we're going to install a gem called [js-routes](https://github.com/railsware/js-routes), this gem defines all Rails named routes as Javascript helpers, and just need to run this command in the console:

```bash
bundle add "js-routes"
```

After installation create this file `config/initializers/jsroutes.rb` with this 👇🏼

```ruby
# config/initializers/jsroutes.rb

JsRoutes.setup do |config|
  config.exclude = [/rails_/] # excludes rails generated routes
  config.compact = true       # removes the _path from the route name
  path = "app/javascript/packs" # destination folder
  JsRoutes.generate!("#{path}/routes.js")
end
```

This help us to generate automatically a file `routes.js` and you can use your Rails routes in Javascript, to use them in all places add the following within `application.js`:

```js
// app/javascript/packs/application.js

...
import Routes from "./routes.js"
Vue.prototype.$routes = Routes
```

We can modify our `Books/Index.vue` file to add the `<inertia-link>` for each book:

```vue
...
<td class="py-3 px-6 text-left">
  <inertia-link :href="$routes.book(book.id)">See</inertia-link>
</td>
```

That is similar to use `book_path(book.id)` in Rails, we are passing the Booki ID to the book URL and finally, we can add the `Books/Show.vue` file:

```vue
<template>
  <div class="text-center">
    <h1 class="text-4xl font-bold mb-5">{{ book.title }}</h1>
    <h2 class="text-xl mb-3">{{ book.author }}</h2>
    <p class="font-light italic mb-5">{{ book.edition }} - {{ book.editorial }}</p>
    <inertia-link :href="$routes.books()" class="text-blue-500">Back</inertia-link>
  </div>
</template>

<script>
  export default {
    props: {
      book: {
        type: Object,
        required: true
      }
    },
  }
</script>
```

To be able to see the changes, go to the browser to `/books` path and click in some Book on the `See` link, and should see the Book view, something like this 👇🏼

![Book page](https://res.cloudinary.com/codeando/image/upload/v1616729472/RailsInertiajs/Screen_Shot_2021-03-25_at_21.28.36_tnymaz.png)

If you open the devtools and navigate to the `Network` tab, choose `All` and then the request with the Book ID (in my case ID 2), yo can see on `Request Headers` section the `X-Inertia: true` and the `X-Requested-With: XMLHttpRequest`:

![Book page](https://res.cloudinary.com/codeando/image/upload/v1616729964/RailsInertiajs/Image_2021-03-25_at_21.35.23_zd9qmz.png)

But remember, if you want to see this on the devtools, you need to come from the `<inertia-link>` element, because if you reload the page, you're doing a full reload page.

So with this, you've implemented the Vue framework on Rails without needing an API or client-side routing, it's just a common Rails application rendering Vue pages 🤟🏼.

See you next! 👋🏼

## Reference

[https://inertiajs.com/](https://inertiajs.com/)

**Read the original post on [codeando.dev](https://codeando.dev/posts/rails-inertiajs/)**