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

2

7

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)?

Mouse answered 29/9, 2015 at 11:41 Comment(2)
Have a look at github.com/rails-api/active_model_serializersGlobeflower
It seems like it'll be more 'efficient' if you don't mix your view and API layer - nothing wrong with making two API calls to the same server to populate your view (esp. if the request doesn't come all the way from the client). The other solutions are more complex and less maintainable.Savarin
F
3

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!

Fistic answered 7/10, 2015 at 3:39 Comment(1)
Thx for the explanation. The following code is still being duplicated in two controllers though. " products = Product.all @products_json = products.map do |product| serializer = Product::APISerializer.new(product, current_user) serializer.as_json end " In my current implementation, I decided to refactor them into concern. Any better solution may be?Mouse
K
1

Try this:

def index
  @products = Product.all
  @products_json render_to_string('/api/v1/products/index', formats: [:json])
  # etc...
end
Kalinda answered 29/9, 2015 at 12:1 Comment(4)
The thing is that the Product.all part can be multiple complex statements. So I don't wish to copy that from the controller API.Mouse
Then why not simply redirect?Kalinda
There could be multiple internal API calls needed to prerender (e.g. @product_json, @user_json)Mouse
@PahleviFikriAuliya you probably need to extract desired functionality from API controller into a separate class/method. It's possible to invoke controller action from another controller, but it's not supposed to be done this way.Kalinda

© 2022 - 2024 — McMap. All rights reserved.