Migrating from jquery-ujs to rails-ujs
Tags: rails, javascript, jquery, ajax
Published: 2019-07-22 07:01:43

Chances are, if you maintain a nontrivial Ruby on Rails application, you already use some **Unobtrusive JavaScript**, or **UJS**. UJS in general is a technique of _not_ inlining your JS code into the HTML markup, but in Rails context specifically it is a small JavaScript library invented by the Rails team that lets you write **almost no code** in some common situations. The library supports:

- confirmation dialogs in links or forms
- non-GET methods in link actions
- auto-disabling buttons in forms to prevent double submitting
- remote forms and links - automatic AJAX calls and responses procesing
- automatic CSRF protection in AJAX requests

The UJS library has been originally written using jQuery some time around 2010 under the name **[`jquery-ujs`](https://github.com/rails/jquery-ujs)**. In 2016, the Rails team **[decided](https://github.com/rails/rails/issues/25208) to drop the jQuery dependency** and rewrite the library into a vanilla-JS one, named **[`rails-ujs`](https://github.com/rails/rails-ujs)**. Soon after that the `rails-ujs` library has been [moved into Rails](https://github.com/rails/rails/tree/master/actionview/app/assets/javascripts) itself, starting with Rails 5.1.

### Why would you want to migrate?

I can see a few reasons that might make you want to switch to `rails-ujs`:

- You might want to take this migration as the **starting point for removing the jQuery dependency** from your project altogether. Not that I have anything against jQuery, but it seems obvious that one of its main purposes — unifying browsers behavior under a single API — is way less relevant than it was years ago, as the modern browsers APIs tend to converge and standardize today.
- You upgraded your Rails application to Rails 5.1 or newer and just want to use UJS that is officially supported as a part of the Rails framework. While the `rails-ujs` code seems to be **[actively developed](https://github.com/rails/rails/pulls?q=is%3Apr+rails-ujs+is%3Aclosed) and maintained**, the original `jquery-ujs` repository is slowly falling behind.
- **You want to use [Stimulus](https://stimulusjs.org/)** for [responding to your remote forms](https://medium.com/parallel-thinking/stimulus-rails-remote-forms-ca5b3e2f02ed) or other UJS actions. If you try that, you will notice that Stimulus does not work together with `jquery-ujs` but it does with `rails-ujs` because it needs native DOM events which [only `rails-ujs` triggers](https://github.com/stimulusjs/stimulus/issues/78).
- Unlike `jquery-ujs`, which is a [single-file library](https://github.com/rails/jquery-ujs/tree/master/src), the current `rails-ujs` is written in a [more modular style](https://github.com/rails/rails/tree/master/actionview/app/assets/javascripts/rails-ujs/features) so **it might be easier for you to read the sources** or contribute, especially if you’re familiar with CoffeeScript (my guess is that UJS will [eventually be rewritten](https://github.com/rails/rails/issues/25208#issuecomment-225033449) to ES6, though).

For our team at [NejRemeslnici](https://www.nejremeslnici.cz), all four points were relevant, but especially the second one made us touch the code after the recent upgrade of our app from Rails 4.2 to 6.0. We want to be closer to active development of the dependencies we use! Convinced, too? Read on!

### Simple things first

If you’re still packing your JS code via the asset pipeline, **just swap the libraries** in the corresponding asset file (otherwise see the [README]() for configuration in webpacker):

```js
// app/assets/javascripts/application.js
-//= require jquery_ujs
+//= require rails-ujs
```

> Note that you should still require the library _after_ `jquery`, so that the automatic CSRF protection still works even in manual AJAX calls made using the [`$.ajax()` jQuery method](https://api.jquery.com/jquery.ajax/).

Next, search through your code for `$.rails.<something>` and replace all occurrences to `Rails.<something>`. This is relevant if you customized the UJS library somehow such as when you used a custom confirmation dialogue or similar.

> Technically speaking, you are not forced to do this replacement because `rails-ujs` recognizes that you still use jQuery and [sets up](https://github.com/rails/rails/blob/master/actionview/app/assets/javascripts/rails-ujs/start.coffee#L15) the `$.rails = Rails` link for you. But why would you want to use that with a vanilla JS library?

Also, it is a good time now to get rid of jQuery stuff in your UJS custom&shy;izations code, if you like.


### Dealing with AJAX event handlers

This is where the migration starts to be a bit tricky…

#### The new event parameters syntax

The syntax for additional parameters of the AJAX event handlers has changed considerably in `rails-ujs`. The event itself is now implemented as a [`CustomEvent`](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent) and all the extra parameters (the `data`, `status` and `xhr`) that were previously sent to the handlers as separate arguments are now **bundled into the `event.detail` attribute** (as an array). So instead of:

```js
$(document).on("ajax:success", function(event, data, status, xhr) {
  ...
});
```

you should write something like:


```js
$(document).on("ajax:success", function(event) {
  var data = event.detail[0];
  var status = event.detail[1];
  var xhr = event.detail[2];
  ...
});
```

The content of the `event.detail` for each particular event type is nicely documented in the [official guides](https://guides.rubyonrails.org/working_with_javascript_in_rails.html#rails-ujs-event-handlers).

#### Migrating jQuery events to UJS events

A small surprise may hit you if you mixed handling [jQuery AJAX events](https://api.jquery.com/Ajax_Events/) (such as `ajaxSuccess`, `ajaxComplete`, etc.) with UJS AJAX calls in your code. Previously you may have used handlers for jQuery events (e.g. `ajaxSuccess`) as well as UJS events (e.g. `ajax:success`) quite interchangeably, since jQuery was always used under the hood and thus **both events were always triggered**. But this is not true any more and you must **convert all jQuery events to the corresponding UJS events**. 

> Of course this only applies to AJAX calls that originate from UJS, e.g. from specifying `remote: true` in your forms. If you use custom AJAX calls made via the jQuery `$.ajax()` method, you should still handle the jQuery events, such as `ajaxSuccess`.

So, for instance, the `ajaxSuccess` jQuery event handler could be rewritten to the UJS variant as follows:

```js
// old form (jQuery event)
$(document).ajaxSuccess(function(event, xhr, options, data) {
  ...
}

// ⟶ new form (UJS event)
$(document).on("ajax:success", function(event) {
  ...
}

```

Note that the jQuery events use _yet another_ set of extra parameters so rewrite them carefully and check the [jQuery docs](https://api.jquery.com/ajaxSuccess/#ajaxSuccess-handler) if unsure.


#### Watch out for proper AJAX data type

In `jquery-ujs`, the AJAX call response body wasn’t processed in any way, it was simply passed to the appropriate event handler. `rails-ujs` however, **[tries](https://github.com/rails/rails/blob/master/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee#L63) to do some basic response processing**:

- it parses JSON responses into a JavaScript object
- it parses HTML / XML / SVG responses into a `Document` object
- most importantly, it **automatically executes JavaScript** responses.

Especially the third option may cause unexpected behavior if you weren’t careful enough for the AJAX data type. By default, `rails-ujs` sets the data type of an AJAX call to `"script"` which tells it to **expect a JavaScript response from the server** and execute the response when received.

Now, if your server tries to send anything else than JavaScript (e.g. some HTML content), the AJAX will suddenly fail as the UJS library tries to execute the JavaScript that it expects. To fix that, either **ensure that the server returns the proper data type** (perhaps a [Server-generated Javascript Response](https://signalvnoise.com/posts/3697-server-generated-javascript-responses), SJR) or **change the expected data type** of the AJAX call via the [`data-type` attribute](https://guides.rubyonrails.org/working_with_javascript_in_rails.html#data-type). The supported data types are `"text"` (for plaintext responses), `"html"`, `"xml"`, `"json"` and the default `"script"` (see the [sources](https://github.com/rails/rails/blob/master/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee#L7) for details).


#### Don’t expect to `trigger()` UJS actions any more

Suppose you have a link with UJS remote handling (via `remote: true` attribute) and you want to “click” the link programmatically. Previously, you could simply call `$('a#my-link').trigger('click')` and this would trigger the AJAX call. In `rails-ujs`, none of this works any more. 

The simple explanation is that the jQuery [`trigger()` function](https://api.jquery.com/trigger/) can only trigger events handled again in jQuery, which they _were_ in `jquery-ujs`. Since `rails-ujs` is completely jQuery-free, you must **convert such code to native events dispatching** instead. In our simple example, you can just call the native [`click()` method](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click) to trigger the AJAX, other times you might want to look at the [`dispatchEvent()` method](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent).


### Final notes

Overall, after going through all this in our code base, the migration felt like a _fixing_ task too, as it forced us to fully understand and correct pieces of code that we didn’t care for enough in the past. That alone justified the migration efforts for us!

I hope the little gotchas described here will not stop you from upgrading your code to `rails-ujs`. They shouldn’t, after you’ve read all this! ;-) Thanks!