Skip to content

Instantly share code, notes, and snippets.

@kohheepeace
Last active April 27, 2024 06:41
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save kohheepeace/c551a4d97e0cf7748465a8321c1bd7dd to your computer and use it in GitHub Desktop.
Save kohheepeace/c551a4d97e0cf7748465a8321c1bd7dd to your computer and use it in GitHub Desktop.
Rails ajax comparison (fetch, Rails.ajax, axios, @rails/request.js, Turbo)

Rails ajax comparison (fetch, Rails.ajax, axios, @rails/request.js, Turbo)

I wrote this gist because I felt that the Rails documentation was lacking a description of ajax requests.

📌 Options for ajax request

There are various ways to send ajax requests in Rails.

  1. Browser default Fetch API
  2. Rails.ajax (No Official docs and request for docs)
  3. http client like axios
  4. @rails/request.js 👈 I'm using this one now !
  5. Turbo 👈 I'm using this one now !

1. Fetch API

[pros]:

  • Browser default http client, so no need to install something.

[cons]:

[Usage]:

const csrfToken = document.getElementsByName("csrf-token")[0].content;

fetch("/posts", {
  method: "POST",
  headers: {
    "X-CSRF-Token": csrfToken,          // 👈👈👈 you need to set token
    "Content-Type": "application/json", // 👈👈👈 To send json in body, specify this
     Accept: "application/json",         // 👈👈👈 Specify the response to be returned as json. For api only mode, this may not be needed
  },
  body: JSON.stringify({ title, body }),
})
  .then((response) => response.json())
  .then((data) => {
    console.log("Success:", data);
  })
  .catch((error) => {
    console.error("Error:", error);
  });

Ref: official docs about how to get csrf token

2. Rails.ajax

source code

[pros]:

  • No need to set csrf token manually
  • Code become short
  • Turbolikns support

[cons]:

  • No official docs
  • you need to send data as URL scheme

[Usage]:

Rails.ajax({
  url: "/posts", // or "/posts.json"
  type: "POST",
  data: `post[title]=${title}&post[body]=${body}`, // 👈👈👈 You need to pass data in URL scheme
  success: function (data) {
    console.log("Success:", data);
  },
  error: function (error) {
    console.error("Error:", error);
  },
});

In app/controllers/posts_controller.rb

# POST /posts or /posts.json
  def create
    @post = Post.new(post_params)

    respond_to do |format|
      if @post.save
        format.html { redirect_to @post, notice: "Post was successfully created." }
        format.json { render :show, status: :created, location: @post }
      else
        format.html { render :new, status: :unprocessable_entity }
        format.json { render json: @post.errors, status: :unprocessable_entity }
      end
    end
  end

In console, response of /posts will look like this.

Success: Turbolinks.clearCache()
Turbolinks.visit("http://localhost:3000/posts/rHBcQ9K-4bssCxz7VB4VXw", {"action":"replace"})

In console, response of /posts.json will look like this.

Success: {id: 139, title: "This is a title", body: "This is a body", user_id: 1, created_at: "2021-06-16T23:28:53.563Z", …}

3. axios

axios is better version of Fetch API (👈 just a opinion). You can write api fetch code cleaner way.

[pros]:

  • Widely used
  • Clean code

[cons]:

[Usage]:

  1. Create custom axios file src/custom-axios.js
import axios from "axios";

const instance = axios.create({
	headers: {
		"Content-Type": "application/json",
		Accept: "application/json",
	},
});

instance.interceptors.request.use(
	function (config) {
		const csrfToken = document.getElementsByName("csrf-token")[0].content;
		config.headers["X-CSRF-Token"] = csrfToken;

		return config;
	},
	function (error) {
		// Do something with request error
		return Promise.reject(error);
	}
);

export default instance;
  1. In js file where you want to call api some-js-file.js
import axios from "../src/custom-axios";

...

axios
  .post("/posts", { title, editorData })
  .then((data) => {
    console.log("Success:", data);
  })
  .catch((error) => {
    console.error("Error:", error);
  });

Refs of axios

Similar http client like axios

4. @rails/request.js

Source

Rails Request.JS encapsulates the logic to send by default some headers that are required by rails applications like the X-CSRF-Token. https://github.com/rails/requestjs-rails

[pros]:

[cons]:

  • Not yet found...

[Usage]:

yarn add @rails/request.js
import { FetchRequest } from '@rails/request.js'

window.FetchRequest = FetchRequest; // 👈 I'm asigning it to window.

const request = new FetchRequest('post',
  'localhost:3000/my_endpoint',
  { body: JSON.stringify({ name: 'Request.JS' }) }
)
const response = await request.perform()
if (response.ok) {
  const body = await response.text
  // Do whatever do you want with the response body
  // You also are able to call `response.html` or `response.json`, be aware that if you call `response.json` and the response contentType isn't `application/json` there will be raised an error.
}

*There is a gem of Request.JS for Rails for Asset Pipeline.

5. Turbo

[Source]

By using Turbo, you don't need to write Javascript, but it works like javascript.

👇 The following Todo apps, which normally require javascript, can be implemented without (or with little) need to write Javascript by using Turbo.

*Credit: https://www.colby.so/posts/turbo-rails-101-todo-list

[Pros]:

  • You don't need to write (much) javascript

[Cons]:

  • Unfamiliar syntax, grammer
  • For simple processes, it may be simpler to write javascript as usual.

✅ Which one should I use?

In this issue, rails team member encourage you to use fetch api, so I decided to use fetch api. rails/rails#38191 (comment)

I decided to use Rails.ajax because I wanted to use Turbulinks. I know that both Rails.ajax and Turbolinks are supposed to be deprecated, but it's easier to use this method for now, and I felt it would be easy to rewrite even if they were deprecated.

I'm using axios now. There is a cons of axios bundle size, however, I chose this one because it gives me the feeling that I am writing clean code.

I am currently using both Turbo and @rails/request.js.

Turbo:

  • Search modals like Algoria (with complex UI changes).

@rails/request.js:

  • Requests to a very simple backend, like "Fav" functionality

📌 About CSRF

CSRF tokens are required if cookies or sessions are used. https://security.stackexchange.com/questions/166724/should-i-use-csrf-protection-on-rest-api-endpoints/166798#166798

📌 Refs

@tclaus
Copy link

tclaus commented Dec 27, 2021

Thanks - I was exactly looking for this last night!
The Rails.ajax thing did not work for me on a fresh Rails 7 project. (Variable not found). So I had to go the fetch way.

@kohheepeace
Copy link
Author

Member of Ruby on Rails commented 👇

Yes, I think the popular opinion is that we want to encourage people to use more standardized APIs (eg. fetch). You can still use rails-ujs, but I don't think we hold any guarantees in terms of API contracts.

rails/rails#38191 (comment)

@kohheepeace
Copy link
Author

kohheepeace commented Mar 8, 2022

@quynhethereal
Copy link

The guide link for obtaining CSRF token from the form for AJAX request is now at https://guides.rubyonrails.org/v6.0.2.1/working_with_javascript_in_rails.html#cross-site-request-forgery-csrf-token-in-ajax

@kohheepeace
Copy link
Author

kohheepeace commented May 21, 2023

*Updated: add @rails/request.js and Turbo.

@kohheepeace
Copy link
Author

How to handle @rails/request.js error rails/request.js#47

Request.JS is just a wrapper around the Fetch API...

const myImage = document.querySelector("img");

const myRequest = new Request("flowers.jpg");

fetch(myRequest)
  .then((response) => {
    if (!response.ok) {
      throw new Error(`HTTP error! Status: ${response.status}`);
    }

    return response.blob();
  })
  .then((response) => {
    myImage.src = URL.createObjectURL(response);
  });

https://developer.mozilla.org/en-US/docs/Web/API/fetch#examples

async function fetchImage() {
  try {
    const response = await fetch("flowers.jpg");
    if (!response.ok) {
      throw new Error("Network response was not OK");
    }
    const myBlob = await response.blob();
    myImage.src = URL.createObjectURL(myBlob);
  } catch (error) {
    console.error("There has been a problem with your fetch operation:", error);
  }
}

https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#checking_that_the_fetch_was_successful

@sergioro9
Copy link

Thanks for the article. What is "Fav" functionality?

@Coridyn
Copy link

Coridyn commented Apr 27, 2024

FYI: It is possible to send a JSON-encoded body with Rails.ajax().

This avoids the issue in your note data: ..., // 👈👈👈 You need to pass data in URL scheme and lets you pass more complex JSON data structures.

The trick is to set the Content-Type: application/json header and JSON-encode the data property.

My solution is to use a custom dataType: "json" property and beforeSend() handler to control this (so I can choose between JSON or form-encoded requests).

Here is an example:

Rails.ajax({
  url: "/posts", // or "/posts.json"
  type: "POST",
  data: { title, body },
  dataType: "json",
  beforeSend: (xhr, options) => {
    if (options.dataType === "json"){
      xhr.setRequestHeader("Content-Type", "application/json");
      if (typeof options.data !== "undefined"){
          options.data = JSON.stringify(options.data);
      }
    }
    return true;
  },
  success: function (data) {
    console.log("Success:", data);
  },
  error: function (error) {
    console.error("Error:", error);
  },
});

It's also possible to add a global beforeSend handler to avoid needing beforeSend on every request object.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment