Research
Security News
Malicious npm Package Targets Solana Developers and Hijacks Funds
A malicious npm package targets Solana developers, rerouting funds in 2% of transactions to a hardcoded address.
Like a React.js component, a Rack::Component
implements a render
method that
takes input data and returns what to display. You can use Components instead of
Controllers, Views, Templates, and Helpers, in any Rack app.
Add rack-component
to your Gemfile and run bundle install
:
gem 'rack-component'
# config.ru
require 'sinatra'
require 'rack/component'
class Hello < Rack::Component
render do |env|
"<h1>Hello, #{h env[:name]}</h1>"
end
end
get '/hello/:name' do
Hello.call(name: params[:name])
end
run Sinatra::Application
Note that Rack::Component does not escape strings by default. To escape
strings, you can either use the #h
helper like in the example above, or you
can configure your components to render a template that escapes automatically.
See the Recipes section for details.
#render
at the instance level instead of via render do
The simplest component is just a lambda that takes an env
parameter:
Greeter = lambda do |env|
"<h1>Hi, #{env[:name]}.</h1>"
end
Greeter.call(name: 'Mina') #=> '<h1>Hi, Mina.</h1>'
Upgrade your lambda to a Rack::Component
when it needs HTML escaping, instance
methods, or state:
require 'rack/component'
class FormalGreeter < Rack::Component
render do |env|
"<h1>Hi, #{h title} #{h env[:name]}.</h1>"
end
# +env+ is available in instance methods too
def title
env[:title] || "Queen"
end
end
FormalGreeter.call(name: 'Franklin') #=> "<h1>Hi, Queen Franklin.</h1>"
FormalGreeter.call(
title: 'Captain',
name: 'Kirk <kirk@starfleet.gov>'
) #=> <h1>Hi, Captain Kirk <kirk@starfleet.gov>.</h1>
Instead of inheriting from Rack::Component
, you can extend
its methods:
class SoloComponent
extend Rack::Component::Methods
render { "Family is complicated" }
end
You can nest Rack::Components as if they were React Children by calling them with a block.
Layout.call(title: 'Home') do
Content.call
end
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 env[:id]
# Nest a PostContent instance inside a Layout instance,
# with some arbitrary HTML too
Layout.call(title: post.title) do
<<~HTML
<main>
#{PostContent.call(title: post.title, body: post.body)}
<footer>
I am a footer.
</footer>
</main>
HTML
end
end
end
class Layout < Rack::Component
# The +render+ macro supports Ruby's keyword arguments, and, like any other
# Ruby function, can accept a block via the & operator.
# Here, :title is a required key in +env+, and &child is just a regular Ruby
# block that could be named anything.
render do |title:, **, &child|
<<~HTML
<!DOCTYPE html>
<html>
<head>
<title>#{h title}</title>
</head>
<body>
#{child.call}
</body>
</html>
HTML
end
end
class PostContent < Rack::Component
render do |title:, body:, **|
<<~HTML
<article>
<h1>#{h title}</h1>
#{h body}
</article>
HTML
end
end
If you add Tilt and erubi
to your Gemfile, you can use the render
macro with an automatically-escaped template instead of a block.
# Gemfile
gem 'tilt'
gem 'erubi'
gem 'rack-component'
# my_component.rb
class TemplateComponent < Rack::Component
render erb: <<~ERB
<h1>Hello, <%= name %></h1>
ERB
def name
env[:name] || 'Someone'
end
end
TemplateComponent.call #=> <h1>Hello, Someone</h1>
TemplateComponent.call(name: 'Spock<>') #=> <h1>Hello, Spock<></h1>
Rack::Component passes { escape_html: true }
to Tilt by default, which enables
automatic escaping in ERB (via erubi) Haml, and Markdown. To disable automatic
escaping, or to pass other tilt options, use an opts: {}
key in render
:
class OptionsComponent < Rack::Component
render opts: { escape_html: false, trim: false }, erb: <<~ERB
<article>
Hi there, <%= {env[:name] %>
<%== yield %>
</article>
ERB
end
Template components support using the yield
keyword to render child
components, but note the double-equals <%==
in the example above. If your
component escapes HTML, and you're yielding to a component that renders HTML,
you probably want to disable escaping via ==
, just for the <%== yield %>
call. This is safe, as long as the component you're yielding to uses escaping.
Using erb
as a key for the inline template is a shorthand, which also works
with haml
and markdown
. But you can also specify engine
and template
explicitly.
require 'haml'
class HamlComponent < Rack::Component
# Note the special HEREDOC syntax for inline Haml templates! Without the
# single-quotes, Ruby will interpret #{strings} before Haml does.
render engine: 'haml', template: <<~'HAML'
%h1 Hi #{env[:name]}.
HAML
end
Using a template instead of raw string interpolation is a safer default, but it
can make it less convenient to do logic while rendering. Feel free to override
your Component's #initialize
method and do logic there:
class EscapedPostView < Rack::Component
def initialize(env)
@post = Post.find(env[:id])
# calling `super` will populate the instance-level `env` hash, making
# `env` available outside this method. But it's fine to skip it.
super
end
render erb: <<~ERB
<article>
<h1><%= @post.title %></h1>
<%= @post.body %>
</article>
ERB
end
JSX Lists use JavaScript's map
function. Rack::Component does
likewise, only you need to call join
on the array:
require 'rack/component'
class PostsList < Rack::Component
render do
<<~HTML
<h1>This is a list of posts</h1>
<ul>
#{render_items}
</ul>
HTML
end
def render_items
env[:posts].map { |post|
<<~HTML
<li class="item">
<a href="/posts/#{post[:id]}">
#{post[:name]}
</a>
</li>
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) #=> <h1>This is a list of posts</h1> <ul>...etc
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def index
render json: PostsList.call(params)
end
end
# app/components/posts_list.rb
class PostsList < Rack::Component
def render
Post.magically_filter_via_params(env).to_json
end
end
Because Rack::Components have the same signature as Rack app, you can mount them anywhere you can mount a Rack app. It's up to you to return a valid rack tuple, though.
# config.ru
require 'rack/component'
class Posts < Rack::Component
def render
[status, headers, [body]]
end
def status
200
end
def headers
{ 'Content-Type' => 'application/json' }
end
def body
Post.all.to_json
end
end
run Posts
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.
#render
at the instance level instead of via render do
The class-level render
macro exists to make using templates easy, and to lean
on Ruby's keyword arguments as a limited imitation of React's defaultProps
and
PropTypes
. But you can define render at the instance level instead.
# these two components render identical output
class MacroComponent < Rack::Component
render do |name:, dept: 'Engineering'|
"#{name} - #{dept}"
end
end
class ExplicitComponent < Rack::Component
def initialize(name:, dept: 'Engineering')
@name = name
@dept = dept
# calling `super` will populate the instance-level `env` hash, making
# `env` available outside this method. But it's fine to skip it.
super
end
def render
"#{@name} - #{@dept}"
end
end
The full API reference is available here:
https://www.rubydoc.info/gems/rack-component
Run ruby spec/benchmarks.rb
to see what to expect in your environment. These
results are from a 2015 iMac:
$ ruby spec/benchmarks.rb
Warming up --------------------------------------
stdlib ERB 2.682k i/100ms
Tilt ERB 15.958k i/100ms
Bare lambda 77.124k i/100ms
RC [def render] 64.905k i/100ms
RC [render do] 57.725k i/100ms
RC [render erb:] 15.595k i/100ms
Calculating -------------------------------------
stdlib ERB 27.423k (± 1.8%) i/s - 139.464k in 5.087391s
Tilt ERB 169.351k (± 2.2%) i/s - 861.732k in 5.090920s
Bare lambda 929.473k (± 3.0%) i/s - 4.705M in 5.065991s
RC [def render] 775.176k (± 1.1%) i/s - 3.894M in 5.024347s
RC [render do] 686.653k (± 2.3%) i/s - 3.464M in 5.046728s
RC [render erb:] 165.113k (± 1.7%) i/s - 826.535k in 5.007444s
Every component in the benchmark is configured to escape HTML when rendering. When rendering via a block, Rack::Component is about 25x faster than ERB and 4x faster than Tilt. When rendering a template via Tilt, it (unsurprisingly) performs roughly at tilt-speed.
When not rendering Tilt templates, 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.
When using Tilt templates, you will need tilt
and a templating gem in your
Gemfile
:
gem 'tilt'
gem 'erubi' # or gem 'haml', etc
gem 'rack-component'
Aye:
Where React uses JSX to make components more ergonomic, Rack::Component leans heavily on some features built into the Ruby language, specifically:
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.
Bug reports and pull requests are welcome on GitHub at https://github.com/chrisfrank/rack-component.
MIT
FAQs
Unknown package
We found that rack-component demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
A malicious npm package targets Solana developers, rerouting funds in 2% of transactions to a hardcoded address.
Security News
Research
Socket researchers have discovered malicious npm packages targeting crypto developers, stealing credentials and wallet data using spyware delivered through typosquats of popular cryptographic libraries.
Security News
Socket's package search now displays weekly downloads for npm packages, helping developers quickly assess popularity and make more informed decisions.