Rack::Component
Like a React.js component, a Rack::Component implements a render method that
takes input data and returns what to display.
bundle add 'rack-component'
Get Started
The simplest component is just a function:
Greeter = lambda do |env|
"<h1>Hi, #{env[:name]}.</h1>"
end
Greeter.call(name: 'Mina') #=> '<h1>Hi, Mina.</h1>'
Convert your function to a Rack::Component when it needs instance methods or state:
require 'rack/component'
class FormalGreeter < Rack::Component
render do |env|
"<h1>Hi, #{title} #{env[:name]}.</h1>"
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') #=> "<h1>Hi, President Macron.</h1>"
FormalGreeter.call(name: 'Merkel', title: 'Chancellor') #=> "<h1>Hi, Chancellor Merkel.</h1>"
Replace #call with #memoized to make re-renders instant:
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 by calling them with a block.
Layout.call(title: 'Home') { Content.call }
Here's a more fully fleshed example:
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
Layout.call(title: post.title) do
PostContent.call(title: post.title, body: post.body)
end
end
end
class PostContent < Rack::Component
render do |env|
" <article>\n <h1>\#{env[:title]}</h1>\n \#{env[:body]}\n </article>\n HTML\n end\nend\n\nclass Layout < Rack::Component\n render do |env, &children|\n # the `&children` param is just a standard ruby block\n <<~HTML\n <!DOCTYPE html>\n <html>\n <head>\n <title>\#{env[:title]}</title>\n </head>\n <body>\n \#{children.call}\n </body>\n </html>\n HTML\n end\nend\n"
Render an HTML list from an array
JSX Lists use JavaScript's map function. Rack::Component does
likewise.
require 'rack/component'
class PostsList < Rack::Component
render do |env|
" <h1>This is a list of posts</h1>\n <ul>\n \#{render_items}\n </ul>\n HTML\n end\n\n def render_items\n env[:posts].map { |post|\n <<~HTML\n <li class=\"item\">\n <a href=\"\#{post[:url]}>\n \#{post[:name]}\n </a>\n </li>\n HTML\n }.join #unlike, with JSX, you need to call `join` on your array\n end\nend\n\nposts = [{ name: 'First Post', id: 1 }, { name: 'Second', id: 2 }]\nPostsList.call(posts: posts) #=> <h1>This is a list of posts</h1> <ul>...etc\n"
Mount a Rack::Component tree inside a Rails app
For when just a few parts of your app are built with components:
# config/routes.rb
mount MyComponent, at: '/a_path_of_your_choosing'
# config/initializers/my_component.rb
require 'rack/component'
class MyComponent < Rack::Component
render do |env|
" <h1>Hello from inside a Rails app!</h1>\n HTML\n end\nend\n"
Build an entire Rack app out of Rack::Components
In real life, maybe don't do this. Use Roda or Sinatra for routing, and use Rack::Component instead of Controllers, Views, and templates. But to see an entire app built only out of Rack::Components, see the example spec.
API Reference
The full API reference is available here:
https://www.rubydoc.info/gems/rack-component
For info on how to clear or change the size of the memoziation cache, please see the spec.
Performance
On my machine, Rendering a Rack::Component is almost 10x faster than rendering a
comparable Tilt template, and almost 100x faster than ERB from the Ruby standard
library. Run ruby spec/benchmarks.rb to see what to expect in your env.
$ ruby spec/benchmarks.rb
Warming up --------------------------------------
Ruby stdlib ERB 2.807k i/100ms
Tilt (cached) 28.611k i/100ms
Lambda 249.958k i/100ms
Component 161.176k i/100ms
Component [memoized] 94.586k i/100ms
Calculating -------------------------------------
Ruby stdlib ERB 29.296k (
Notice that using Component#memoized is slower than using Component#call
in this benchmark. Because these components do almost nothing, it's more work to
check the memoziation cache than to just render. For components that don't
access a database, don't do network I/O, and aren't very CPU-intensive, it's
probably fastest not to memoize. For components that do I/O, using #memoize
can speed things up by several orders of magnitude.
Compatibility
Rack::Component has zero dependencies, and will work in any Rack app. It should
even work outside a Rack app, because it's not actually dependent on Rack. I
packaged it under the Rack namespace because it follows the Rack call
specification, and because that's where I use and test it.
Anybody using this in production?
Aye:
Ruby reference:
Where React uses JSX to make components more ergonomic, Rack::Component leans heavily on some features built into the Ruby language, specifically:
Development
After checking out the repo, run bin/setup to install dependencies. Then, run
rake spec to run the tests. You can also run bin/console for an interactive
prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install. To
release a new version, update the version number in version.rb, and then run
bundle exec rake release, which will create a git tag for the version, push
git commits and tags, and push the .gem file to
rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/chrisfrank/rack-component.
License
MIT