# Rack::Component Like a React.js component, a `Rack::Component` implements a `render` method that takes input data and returns what to display. ## Install Add `rack-component` to your Gemfile and run `bundle install`: ``` gem 'rack-component' ``` ## Table of Contents * [Getting Started](#getting-started) * [Components as plain functions](#components-as-plain-functions) * [Components as Rack::Components](#components-as-rackcomponents) * [Components that re-render instantly](#components-that-re-render-instantly) * [Recipes](#recipes) * [Render one component inside another](#render-one-component-inside-another) * [Memoize an expensive component for one minute](#memoize-an-expensive-component-for-one-minute) * [Memoize an expensive component until its content changes](#memoize-an-expensive-component-until-its-content-changes) * [Render an HTML list from an array](#render-an-html-list-from-an-array) * [Render a Rack::Component from a Rails controller](#render-a-rackcomponent-from-a-rails-controller) * [Mount a Rack::Component as a Rack app](#mount-a-rackcomponent-as-a-rack-app) * [Build an entire App out of Rack::Components](#build-an-entire-app-out-of-rackcomponents) * [API Reference](#api-reference) * [Performance](#performance) * [Compatibility](#compatibility) * [Anybody using this in production?](#anybody-using-this-in-production) * [Ruby reference](#ruby-reference) * [Development](#development) * [Contributing](#contributing) * [License](#license) ## Getting Started ### Components as plain functions The simplest component is just a lambda that takes an `env` parameter: ```ruby Greeter = lambda do |env| "

Hi, #{env[:name]}.

" end Greeter.call(name: 'Mina') #=> '

Hi, Mina.

' ``` ### Components as Rack::Components Convert your lambda to a `Rack::Component` when it needs instance methods or state: ```ruby require 'rack/component' class FormalGreeter < Rack::Component render do |env| "

Hi, #{title} #{env[:name]}.

" end def title # the hash you pass to `call` is available as `env` in instance methods env[:title] || "President" end end FormalGreeter.call(name: 'Macron') #=> "

Hi, President Macron.

" FormalGreeter.call(name: 'Merkel', title: 'Chancellor') #=> "

Hi, Chancellor Merkel.

" ``` ### Components that re-render instantly Replace `#call` with `#memoized` to make re-renders with the same `env` instant: ```ruby require 'rack/component' require 'net/http' class NetworkGreeter < Rack::Component render do |env| "Hi, #{get_job_title_from_api} #{env[:name]}." end def get_job_title_from_api endpoint = URI("http://api.heads-of-state.gov/") Net::HTTP.get("#{endpoint}?q=#{env[:name]}") end end NetworkGreeter.memoized(name: 'Macron') # ...after a slow network call to our fictional Heads Of State API #=> "Hi, President Macron." NetworkGreeter.memoized(name: 'Macron') # subsequent calls with the same env are instant. #=> "Hi, President Macron." NetworkGreeter.memoized(name: 'Merkel') # ...this env is new, so NetworkGreeter makes another network call #=> "Hi, Chancellor Merkel." NetworkGreeter.memoized(name: 'Merkel') #=> instant! "Hi, Chancellor Merkel." NetworkGreeter.memoized(name: 'Macron') #=> instant! "Hi, President Macron." ``` ## Recipes ### Render one component inside another You can nest Rack::Components as if they were [React Children][jsx children] by calling them with a block. ```ruby Layout.call(title: 'Home') { Content.call } ``` Here's a more fully fleshed example: ```ruby require 'rack/component' # let's say this is a Sinatra app: get '/posts/:id' do PostPage.call(id: params[:id]) end # fetch a post from the database and render it inside a layout class PostPage < Rack::Component render do |env| post = Post.find(id: env[:id]) # Nest a PostContent instance inside a Layout instance, with some arbitrary HTML too Layout.call(title: post.title) do <<~HTML
#{PostContent.call(title: post.title, body: post.body)}
HTML end end end class PostContent < Rack::Component render do |env| <<~HTML

#{env[:title]}

#{env[:body]}
HTML end end class Layout < Rack::Component render do |env, &children| # the `&children` param is just a standard ruby block <<~HTML #{env[:title]} #{children.call} HTML end end ``` ### Memoize an expensive component for one minute You can use `memoized` as a time-based cache by passing a timestamp to `env`: ```ruby require 'rack/component' # Render one million posts as JSON class MillionPosts < Rack::Component render { |env| Post.limit(1_000_000).to_json } end MillionPosts.memoized(Time.now.to_i / 60) #=> first call is slow MillionPosts.memoized(Time.now.to_i / 60) #=> next calls in same minute are quick ``` ### Memoize an expensive component until its content changes This recipe will speed things up when your database calls are fast but your render method is slow: ```ruby require 'rack/component' class PostAnalysis < Rack::Component render do |env| <<~HTML

#{env[:post].title}

#{env[:post].content}
HTML end def expensive_natural_language_analysis FancyNaturalLanguageLibrary.analyze(env[:post].content) end end PostAnalysis.memoized(post: Post.find(1)) #=> slow, because it runs an expensive natural language analysis PostAnalysis.memoized(post: Post.find(1)) #=> instant, because the content of :post has not changed ``` This recipe works with any Ruby object that implements a `#hash` method based on the object's content, including instances of `ActiveRecord::Base` and `Sequel::Model`. ### Render an HTML list from an array [JSX Lists][jsx lists] use JavaScript's `map` function. Rack::Component does likewise, only you need to call `join` on the array: ```ruby require 'rack/component' class PostsList < Rack::Component render do |env| <<~HTML

This is a list of posts

HTML end def render_items env[:posts].map { |post| <<~HTML
  • #{post[:name]}
  • HTML }.join #unlike JSX, you need to call `join` on your array end end posts = [{ name: 'First Post', id: 1 }, { name: 'Second', id: 2 }] PostsList.call(posts: posts) #=>

    This is a list of posts