{"id":22535895,"url":"https://github.com/chrisfrank/rack-component","last_synced_at":"2025-10-18T11:35:42.849Z","repository":{"id":56890087,"uuid":"153536231","full_name":"chrisfrank/rack-component","owner":"chrisfrank","description":"Handle HTTP requests with modular, React-style components, in any Rack app","archived":false,"fork":false,"pushed_at":"2019-04-15T17:15:31.000Z","size":97,"stargazers_count":68,"open_issues_count":0,"forks_count":2,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-03-23T21:03:00.586Z","etag":null,"topics":["component","rails","react","ruby","views"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/chrisfrank.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-10-17T23:27:26.000Z","updated_at":"2025-02-10T17:43:30.000Z","dependencies_parsed_at":"2022-08-21T00:50:23.885Z","dependency_job_id":null,"html_url":"https://github.com/chrisfrank/rack-component","commit_stats":null,"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chrisfrank%2Frack-component","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chrisfrank%2Frack-component/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chrisfrank%2Frack-component/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chrisfrank%2Frack-component/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/chrisfrank","download_url":"https://codeload.github.com/chrisfrank/rack-component/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247615375,"owners_count":20967184,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["component","rails","react","ruby","views"],"created_at":"2024-12-07T10:08:59.869Z","updated_at":"2025-10-18T11:35:37.828Z","avatar_url":"https://github.com/chrisfrank.png","language":"Ruby","readme":"# Rack::Component\n\nLike a React.js component, a `Rack::Component` implements a `render` method that\ntakes input data and returns what to display. You can use Components instead of\nControllers, Views, Templates, and Helpers, in any Rack app.\n\n## Install\n\nAdd `rack-component` to your Gemfile and run `bundle install`:\n\n```\ngem 'rack-component'\n```\n\n## Quickstart with Sinatra\n\n```ruby\n# config.ru\nrequire 'sinatra'\nrequire 'rack/component'\n\nclass Hello \u003c Rack::Component\n  render do |env|\n    \"\u003ch1\u003eHello, #{h env[:name]}\u003c/h1\u003e\"\n  end\nend\n\nget '/hello/:name' do\n  Hello.call(name: params[:name])\nend\n\nrun Sinatra::Application\n```\n\n**Note that Rack::Component does not escape strings by default**. To escape\nstrings, you can either use the `#h` helper like in the example above, or you\ncan configure your components to render a template that escapes automatically.\nSee the [Recipes](#recipes) section for details.\n\n## Table of Contents\n\n* [Getting Started](#getting-started)\n  * [Components as plain functions](#components-as-plain-functions)\n  * [Components as Rack::Components](#components-as-rackcomponents)\n    * [Components if you hate inheritance](#components-if-you-hate-inheritance)\n* [Recipes](#recipes)\n  * [Render one component inside another](#render-one-component-inside-another)\n  * [Render a template that escapes output by default via Tilt](#render-a-template-that-escapes-output-by-default-via-tilt)\n  * [Render an HTML list from an array](#render-an-html-list-from-an-array)\n  * [Render a Rack::Component from a Rails controller](#render-a-rackcomponent-from-a-rails-controller)\n  * [Mount a Rack::Component as a Rack app](#mount-a-rackcomponent-as-a-rack-app)\n  * [Build an entire App out of Rack::Components](#build-an-entire-app-out-of-rackcomponents)\n  * [Define `#render` at the instance level instead of via `render do`](#define-render-at-the-instance-level-instead-of-via-render-do)\n* [API Reference](#api-reference)\n* [Performance](#performance)\n* [Compatibility](#compatibility)\n* [Anybody using this in production?](#anybody-using-this-in-production)\n* [Ruby reference](#ruby-reference)\n* [Development](#development)\n* [Contributing](#contributing)\n* [License](#license)\n\n## Getting Started\n\n### Components as plain functions\n\nThe simplest component is just a lambda that takes an `env` parameter:\n\n```ruby\nGreeter = lambda do |env|\n  \"\u003ch1\u003eHi, #{env[:name]}.\u003c/h1\u003e\"\nend\n\nGreeter.call(name: 'Mina') #=\u003e '\u003ch1\u003eHi, Mina.\u003c/h1\u003e'\n```\n\n### Components as Rack::Components\n\nUpgrade your lambda to a `Rack::Component` when it needs HTML escaping, instance\nmethods, or state:\n\n```ruby\nrequire 'rack/component'\nclass FormalGreeter \u003c Rack::Component\n  render do |env|\n    \"\u003ch1\u003eHi, #{h title} #{h env[:name]}.\u003c/h1\u003e\"\n  end\n\n  # +env+ is available in instance methods too\n  def title\n    env[:title] || \"Queen\"\n  end\nend\n\nFormalGreeter.call(name: 'Franklin') #=\u003e \"\u003ch1\u003eHi, Queen Franklin.\u003c/h1\u003e\"\nFormalGreeter.call(\n  title: 'Captain',\n  name: 'Kirk \u003ckirk@starfleet.gov\u003e'\n) #=\u003e \u003ch1\u003eHi, Captain Kirk \u0026lt;kirk@starfleet.gov\u0026gt;.\u003c/h1\u003e\n```\n\n#### Components if you hate inheritance\n\nInstead of inheriting from `Rack::Component`, you can `extend` its methods:\n\n```ruby\nclass SoloComponent\n  extend Rack::Component::Methods\n  render { \"Family is complicated\" }\nend\n```\n\n## Recipes\n\n### Render one component inside another\n\nYou can nest Rack::Components as if they were [React Children][jsx children] by\ncalling them with a block.\n\n```ruby\nLayout.call(title: 'Home') do\n  Content.call\nend\n```\n\nHere's a more fully fleshed example:\n\n```ruby\nrequire 'rack/component'\n\n# let's say this is a Sinatra app:\nget '/posts/:id' do\n  PostPage.call(id: params[:id])\nend\n\n# Fetch a post from the database and render it inside a Layout\nclass PostPage \u003c Rack::Component\n  render do |env|\n    post = Post.find env[:id]\n    # Nest a PostContent instance inside a Layout instance,\n    # with some arbitrary HTML too\n    Layout.call(title: post.title) do\n      \u003c\u003c~HTML\n        \u003cmain\u003e\n          #{PostContent.call(title: post.title, body: post.body)}\n          \u003cfooter\u003e\n            I am a footer.\n          \u003c/footer\u003e\n        \u003c/main\u003e\n      HTML\n    end\n  end\nend\n\nclass Layout \u003c Rack::Component\n  # The +render+ macro supports Ruby's keyword arguments, and, like any other\n  # Ruby function, can accept a block via the \u0026 operator.\n  # Here, :title is a required key in +env+, and \u0026child is just a regular Ruby\n  # block that could be named anything.\n  render do |title:, **, \u0026child|\n    \u003c\u003c~HTML\n      \u003c!DOCTYPE html\u003e\n      \u003chtml\u003e\n        \u003chead\u003e\n          \u003ctitle\u003e#{h title}\u003c/title\u003e\n        \u003c/head\u003e\n        \u003cbody\u003e\n        #{child.call}\n        \u003c/body\u003e\n      \u003c/html\u003e\n    HTML\n  end\nend\n\nclass PostContent \u003c Rack::Component\n  render do |title:, body:, **|\n    \u003c\u003c~HTML\n      \u003carticle\u003e\n        \u003ch1\u003e#{h title}\u003c/h1\u003e\n        #{h body}\n      \u003c/article\u003e\n    HTML\n  end\nend\n```\n\n### Render a template that escapes output by default via Tilt\n\nIf you add [Tilt][tilt] and `erubi` to your Gemfile, you can use the `render`\nmacro with an automatically-escaped template instead of a block.\n\n```ruby\n# Gemfile\ngem 'tilt'\ngem 'erubi'\ngem 'rack-component'\n\n# my_component.rb\nclass TemplateComponent \u003c Rack::Component\n  render erb: \u003c\u003c~ERB\n    \u003ch1\u003eHello, \u003c%= name %\u003e\u003c/h1\u003e\n  ERB\n\n  def name\n    env[:name] || 'Someone'\n  end\nend\n\nTemplateComponent.call #=\u003e \u003ch1\u003eHello, Someone\u003c/h1\u003e\nTemplateComponent.call(name: 'Spock\u003c\u003e') #=\u003e \u003ch1\u003eHello, Spock\u0026lt;\u0026gt;\u003c/h1\u003e\n```\n\nRack::Component passes `{ escape_html: true }` to Tilt by default, which enables\nautomatic escaping in ERB (via erubi) Haml, and Markdown. To disable automatic\nescaping, or to pass other tilt options, use an `opts: {}` key in `render`:\n\n```ruby\nclass OptionsComponent \u003c Rack::Component\n  render opts: { escape_html: false, trim: false }, erb: \u003c\u003c~ERB\n    \u003carticle\u003e\n      Hi there, \u003c%= {env[:name] %\u003e\n      \u003c%== yield %\u003e\n    \u003c/article\u003e\n  ERB\nend\n```\n\nTemplate components support using the `yield` keyword to render child\ncomponents, but note the double-equals `\u003c%==` in the example above. If your\ncomponent escapes HTML, and you're yielding to a component that renders HTML,\nyou probably want to disable escaping via `==`, just for the `\u003c%== yield %\u003e`\ncall. This is safe, as long as the component you're yielding to uses escaping.\n\nUsing `erb` as a key for the inline template is a shorthand, which also works\nwith `haml` and `markdown`. But you can also specify `engine` and `template`\nexplicitly.\n\n```ruby\nrequire 'haml'\nclass HamlComponent \u003c Rack::Component\n  # Note the special HEREDOC syntax for inline Haml templates! Without the\n  # single-quotes, Ruby will interpret #{strings} before Haml does.\n  render engine: 'haml', template: \u003c\u003c~'HAML'\n    %h1 Hi #{env[:name]}.\n  HAML\nend\n```\n\nUsing a template instead of raw string interpolation is a safer default, but it\ncan make it less convenient to do logic while rendering. Feel free to override\nyour Component's `#initialize` method and do logic there:\n\n```ruby\nclass EscapedPostView \u003c Rack::Component\n  def initialize(env)\n    @post = Post.find(env[:id])\n    # calling `super` will populate the instance-level `env` hash, making\n    # `env` available outside this method. But it's fine to skip it.\n    super\n  end\n\n  render erb: \u003c\u003c~ERB\n    \u003carticle\u003e\n      \u003ch1\u003e\u003c%= @post.title %\u003e\u003c/h1\u003e\n      \u003c%= @post.body %\u003e\n    \u003c/article\u003e\n  ERB\nend\n```\n\n### Render an HTML list from an array\n\n[JSX Lists][jsx lists] use JavaScript's `map` function. Rack::Component does\nlikewise, only you need to call `join` on the array:\n\n```ruby\nrequire 'rack/component'\nclass PostsList \u003c Rack::Component\n  render do\n    \u003c\u003c~HTML\n      \u003ch1\u003eThis is a list of posts\u003c/h1\u003e\n      \u003cul\u003e\n        #{render_items}\n      \u003c/ul\u003e\n    HTML\n  end\n\n  def render_items\n    env[:posts].map { |post|\n      \u003c\u003c~HTML\n        \u003cli class=\"item\"\u003e\n          \u003ca href=\"/posts/#{post[:id]}\"\u003e\n            #{post[:name]}\n          \u003c/a\u003e\n        \u003c/li\u003e\n      HTML\n    }.join # unlike 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) #=\u003e \u003ch1\u003eThis is a list of posts\u003c/h1\u003e \u003cul\u003e...etc\n```\n\n### Render a Rack::Component from a Rails controller\n\n```ruby\n# app/controllers/posts_controller.rb\nclass PostsController \u003c ApplicationController\n  def index\n    render json: PostsList.call(params)\n  end\nend\n\n# app/components/posts_list.rb\nclass PostsList \u003c Rack::Component\n  def render\n    Post.magically_filter_via_params(env).to_json\n  end\nend\n```\n\n### Mount a Rack::Component as a Rack app\n\nBecause Rack::Components have the same signature as Rack app, you can mount them\nanywhere you can mount a Rack app. It's up to you to return a valid rack tuple,\nthough.\n\n```ruby\n# config.ru\nrequire 'rack/component'\n\nclass Posts \u003c Rack::Component\n  def render\n    [status, headers, [body]]\n  end\n\n  def status\n    200\n  end\n\n  def headers\n    { 'Content-Type' =\u003e 'application/json' }\n  end\n\n  def body\n    Post.all.to_json\n  end\nend\n\nrun Posts\n```\n\n### Build an entire App out of Rack::Components\n\nIn real life, maybe don't do this. Use [Roda] or [Sinatra] for routing, and use\nRack::Component instead of Controllers, Views, and templates. But to see an\nentire app built only out of Rack::Components, see\n[the example spec](https://github.com/chrisfrank/rack-component/blob/master/spec/raw_rack_example_spec.rb).\n\n### Define `#render` at the instance level instead of via `render do`\n\nThe class-level `render` macro exists to make using templates easy, and to lean\non Ruby's keyword arguments as a limited imitation of React's `defaultProps` and\n`PropTypes`. But you can define render at the instance level instead.\n\n```ruby\n# these two components render identical output\n\nclass MacroComponent \u003c Rack::Component\n  render do |name:, dept: 'Engineering'|\n    \"#{name} - #{dept}\"\n  end\nend\n\nclass ExplicitComponent \u003c Rack::Component\n  def initialize(name:, dept: 'Engineering')\n    @name = name\n    @dept = dept\n    # calling `super` will populate the instance-level `env` hash, making\n    # `env` available outside this method. But it's fine to skip it.\n    super\n  end\n\n  def render\n    \"#{@name} - #{@dept}\"\n  end\nend\n```\n\n## API Reference\n\nThe full API reference is available here:\n\nhttps://www.rubydoc.info/gems/rack-component\n\n## Performance\n\nRun `ruby spec/benchmarks.rb` to see what to expect in your environment. These\nresults are from a 2015 iMac:\n\n```\n$ ruby spec/benchmarks.rb\nWarming up --------------------------------------\n          stdlib ERB     2.682k i/100ms\n            Tilt ERB    15.958k i/100ms\n         Bare lambda    77.124k i/100ms\n     RC [def render]    64.905k i/100ms\n      RC [render do]    57.725k i/100ms\n    RC [render erb:]    15.595k i/100ms\nCalculating -------------------------------------\n          stdlib ERB     27.423k (± 1.8%) i/s -    139.464k in   5.087391s\n            Tilt ERB    169.351k (± 2.2%) i/s -    861.732k in   5.090920s\n         Bare lambda    929.473k (± 3.0%) i/s -      4.705M in   5.065991s\n     RC [def render]    775.176k (± 1.1%) i/s -      3.894M in   5.024347s\n      RC [render do]    686.653k (± 2.3%) i/s -      3.464M in   5.046728s\n    RC [render erb:]    165.113k (± 1.7%) i/s -    826.535k in   5.007444s\n```\n\nEvery component in the benchmark is configured to escape HTML when rendering.\nWhen rendering via a block, Rack::Component is about 25x faster than ERB and 4x\nfaster than Tilt. When rendering a template via Tilt, it (unsurprisingly)\nperforms roughly at tilt-speed.\n\n## Compatibility\n\nWhen not rendering Tilt templates, Rack::Component has zero dependencies,\nand will work in any Rack app. It should even work _outside_ a Rack app, because\nit's not actually dependent on Rack. I packaged it under the Rack namespace\nbecause it follows the Rack `call` specification, and because that's where I\nuse and test it.\n\nWhen using Tilt templates, you will need `tilt` and a templating gem in your\n`Gemfile`:\n\n```ruby\ngem 'tilt'\ngem 'erubi' # or gem 'haml', etc\ngem 'rack-component'\n```\n\n## Anybody using this in production?\n\nAye:\n\n* [future.com](https://www.future.com/)\n* [Seattle \u0026 King County Homelessness Response System](https://hrs.kc.future.com/)\n\n## Ruby reference\n\nWhere React uses [JSX] to make components more ergonomic, Rack::Component leans\nheavily on some features built into the Ruby language, specifically:\n\n* [Heredocs]\n* [String Interpolation]\n* [Calling methods with a block][ruby blocks]\n\n## Development\n\nAfter checking out the repo, run `bin/setup` to install dependencies. Then, run\n`rake spec` to run the tests. You can also run `bin/console` for an interactive\nprompt that will allow you to experiment.\n\nTo install this gem onto your local machine, run `bundle exec rake install`. To\nrelease a new version, update the version number in `version.rb`, and then run\n`bundle exec rake release`, which will create a git tag for the version, push\ngit commits and tags, and push the `.gem` file to\n[rubygems.org](https://rubygems.org).\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at\nhttps://github.com/chrisfrank/rack-component.\n\n## License\n\nMIT\n\n[spec]: https://github.com/chrisfrank/rack-component/blob/master/spec/rack/component_spec.rb\n[jsx]: https://reactjs.org/docs/introducing-jsx.html\n[jsx children]: https://reactjs.org/docs/composition-vs-inheritance.html\n[jsx lists]: https://reactjs.org/docs/lists-and-keys.html\n[heredocs]: https://ruby-doc.org/core-2.5.0/doc/syntax/literals_rdoc.html#label-Here+Documents\n[string interpolation]: http://ruby-for-beginners.rubymonstas.org/bonus/string_interpolation.html\n[ruby blocks]: https://mixandgo.com/learn/mastering-ruby-blocks-in-less-than-5-minutes\n[roda]: http://roda.jeremyevans.net\n[sinatra]: http://sinatrarb.com\n[tilt]: https://github.com/rtomayko/tilt\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchrisfrank%2Frack-component","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fchrisfrank%2Frack-component","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchrisfrank%2Frack-component/lists"}