Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to call internal API from Rails view (for ReactJS prerender purpose)?

I already have Rails API controller which return JSON response. It is used by front-end Javascript (as well as mobile app) to render values.

Now, I wish to prerender those values using ReactJS:

#app/controllers/api/v1/products_controller.rb
module API
    module V1
        class ProductsController < ApplicationController
            def index
                @products = Product.all #this could  be acomplex multi-line statements. 
                #rendered in api/v1/products/index.json.jbuilder
            end
        end
    end
end

#app/controllers/products_controller.rb
class ProductsController < ApplicationController
    def index
        #How to do this efficiently?
        @products_json = #Call to internal /api/v1/products/index for prerender purpose.
        @user_json = #Call to internal /api/v1/user/show for prerender purpose.
    end
end

#app/views/products/index.html.erb
<%= react_component('ProductsList', @products_json, {prerender: true}) %>
<%= react_component('UserProfile', @user_json, {prerender: true}) %>

How do I call internal /api/v1/products and /api/v1/user URL efficiently (e.g. without making HTTP GET request to my own server)?

like image 789
Pahlevi Fikri Auliya Avatar asked Sep 29 '15 11:09

Pahlevi Fikri Auliya


2 Answers

I agree with your desire to reuse your API code for your views. That will make the application much more maintainable.

What if you changed the scope a little bit? Instead of calling a controller method, move the logic into a new Ruby class.

This class's job is to turn an object into a JSON string, so it's called a "serializer". In my app, we have app/serializers/{model_name}/ for storing different serializer classes.

Here's an example serializer:

# app/serializers/product/api_serializer.rb
class Product::APISerializer 
  attr_reader :product, :current_user 

  def initialize(product, current_user)
    @product = product 
    @current_user = current_user
  end 

  # Return a hash representation for your object
  def as_json(options={}) # Rails uses this API
    {
      name: product.name,
      description: product.description,
      price: localized_price,
      categories: product.categories.map { |c| serialize_category(c) },
      # ... all your JSON values
    }
  end 

  private 

  # For example, you can put logic in private methods of this class.
  def localized_price 
    current_currency = current_user.currency
    product.price.convert_to(current_currency)
  end 

  def serialize_category(category)
    { name: category.name }
  end
end

Then, use this serializer to build your API response:

class API::V1::ProductsController < ApplicationController 
  def index 
    products = Product.all 
    products_json = products.map do |product|
      serializer = Product::APISerializer.new(product, current_user)
      serializer.as_json
    end 
    render json: products_json
  end
end

Then, you can use the serializer again in the UI controller:

class ProductsController < ApplicationController 
  def index 
    products = Product.all 
    @products_json = products.map do |product|
      serializer = Product::APISerializer.new(product, current_user)
      serializer.as_json
    end 
    # render view ... 
  end
end

Because you used the same serializer in both cases, the JSON representation of the products will be the same!

There are a few advantages to this approach:

  • Because your serializer is a plain Ruby class, it's easy to write & test
  • It's easy to share the JSON logic between controllers
  • It's very extensible: when you need JSON for a different purpose, simply add a new serializer and use it.

Some people use ActiveModel Serializers for this, but I don't. I tried AMS a year ago and I didn't like it because it overrides as_json for all objects in your app, which caused breaking changes in my case!

like image 200
rmosolgo Avatar answered Nov 04 '22 21:11

rmosolgo


Try this:

def index
  @products = Product.all
  @products_json render_to_string('/api/v1/products/index', formats: [:json])
  # etc...
end
like image 22
dimakura Avatar answered Nov 04 '22 21:11

dimakura