This is a casual write-up of things I learn while building with Hotwire and Rails 7. As well as my notes and thoughts on Ruby and Rails content I consume (books, conference talks).

Below you'll find

  • Difference between rails-ujs, request.js and mrujs and when to use each
  • to_param method from ActiveRecord::Base to get better url slugs
  • Sustainable Web Development with Ruby on Rails book notes

If you're familiar with Rails but new to Turbo and modern frontend stuff with Rails 7, you'll get the most value out of this. If you are new to Rails or new to programming, you will still get some value from this one and I encourage you to read it.

No more RailsUJS? What are Request.js and Mrujs?

Rails 7 officially removed including rails-ujs by default. So does that mean we should stop using rails-ujs then? And what are Mrujs and Request.js that I hear about?

Quick detour on the history of rails-ujs:

  • rails-ujs is a rewrite of the jquery-ujs gem without the jquery dependency. (ujs = unobtrusive javascript. Basically keep js out of the markup and add 'data' attributes to the html to add js behavior)

  • rails-ujs was moved into Rails itself in Rails 5.1.0.

  • Also rails-ujs is written in CoffeeScript apparently - we can read the code here

  • rails-ujs allows us to do the following things:

    • make non-GET requests from <a> links (e.g. data-method="delete")
    • make forms, links and buttons submit data asynchronously with Ajax
    • add confirmation dialogs for actions
    • disable submit button after form submission

In case you're not familiar, This article goes into more detail with rails-ujs examples

Okay but since rails-ujs is not included with Rails 7, what are we suppose to do? We're suppose to use Turbo instead. This basically implies instead of data-method="delete" we need to add data-turbo-metho="delete". In our ERB templates it looks like this:
<%= link_to "Delete post", post_path(post), data: { turbo_method: "delete" } %>

That seems like a simple enough change from using rails-ujs to Turbo. So what is mrujs for then?

Well, the change to using Turbo is minor for new projects but imagine migrating an existing project with hundreds of links with data-method and such from rails-ujs to Rails 7 Turbo. It would be kind of a pain. Mrujs was created to solve that. According to the mrujs site it strives to be a drop-in replacement for rails-ujs. Meaning we can keep doing data-method and all that. So mrujs is a successor to rails-ujs that is maintained.

What about request.js then? Mrujs is for replacing all of UJS. If you're only using AJAX, request.js may make sense. With request.js, AJAX call in stimulus controller goes from this

Rails.ajax({
      url: this.data.get("url").replace(":id", id),
      type: 'PATCH',
      data: data
    })

to...this (after adding the npm package)

import { patch } from '@rails/request.js'
...
patch(this.data.get("url").replace(":id", id), {body: data})

Here is more detail about how to use request.js

So in summary, we should move to Turbo for UJS functionality in Rails 7 applications. For existing applications or while migrating we can keep using rails-ujs. But if we want a replacement for rails-ujs that is maintained we can use mrujs. And if are only need AJAX part of rails-ujs, we can also use request.js

Method: to_param for Rails models

Sometimes in our Rails app, it would be nice to get a more expressive url like this /posts/post-5-hello-world instead of the usual posts/5.

We can do this by defining the to_params method in our model. In the definition below, we include the post's title in addition to the post id. So instead of posts/5 we'll see posts/5-hello-world in the browser.

def to_param
  "#{id}-#{title.downcase.to_s}".parameterize
end

If our model does not define this method then it will use the implementation in ActiveRecord::Base which just returns the id so we get usual /posts/:id.

Book: Sustainable Rails

I read the chapter "Business Logic does not go in ActiveRecords". Here's my takeaway.

The author makes the case for putting business logic (the code that is specific to your app, that implements what the app is suppose to do for the user) in separate classes that 'go between' the Controller and ActiveRecord/Model classes. Calling these service classes and putting them in app/service.

Here is the logical argument used to make the case for the above. Point 1 business logic is a magnet for complexity. Business logic is also less stable and has high churn (as we iterate and learn more about our biz and domain). So this implies that business logic code is prone to more bugs.

Point 2 a bug in some class will effect all the classes that call that class (fan-in). And ActiveRecord is central of our app and called by many places. So a bug in a ActiveRecord class will effect all those other classes that call it.

Therefore, it makes sense to not put business logic in ActiveRecords but rather to isolate it in it's own service classes. Then the impact of bugs would be isolated to less of the system.

Alright, I see the logic of the arguments agaist putting business logic in model classes. And I buy it. One thing comes to mind that may be considered a con for this approach - to read code and understand how some part of a system works, we may now need to open dozen service files and read the one or two methods in each. We had something like this at my previous job (non-rails codebase). Still a worthwhile trade-off.

Aside: There is a RailsConf 2022 talk titled "Your Service Layer Needn't be Fancy, It Just Needs to Exist" by the author. It's worth a watch because it's funny. This one thing from the talk highly resonates with my experience: Generic advice around "clean code and "best practices" principles are not useful (ex: skinny controller, fat model) because "they are super vague and your team will argue about what these mean instead of getting their work done" hahah.

Subscribe to Build With Hotwire Newsletter

Oh hey! you've made it to the end, hope you enjoyed the read. If you want to get this in your inbox, here's the box and a button. You know what to do!

* indicates required