What is it, naokirin?

Ruby on Rails(ViewComponent)に Storybook を導入してみる

(個人的な観測範囲からですが)React界隈を中心にデザインシステムなどとともに、Storybookを導入している企業が増えてきているように思います。

自分は直近では、Ruby on RailsSSRをメインに開発をしていることが多いため、導入がやや難しいと感じていました。

しかし、ViewComponentを導入していると比較的簡単に導入可能であることを知ったので、今回試してみます。

今回は bootstrapが入っている状態で検証をしているので、予め承知ください。

検証したバージョン

  • ruby (3.1.0)
  • rails (6.1.7)
  • view_component (2.80.0)
  • view_component_storybook (0.12.0)

ViewComponent、ViewComponent::Storybook の導入

そもそもですが、ViewComponentは、GitHubの開発者がメインとなって開発したOSSです。
コンポーネントをこれまでのRailsでは、View部分のHTMLテンプレート(ERb、HAMLなど)のみでしか扱えなかったのですが、テンプレートとRubyオブジェクトのセットとして書くことができるようになります。

github.com

今回は、ViewComponentの紹介ではないので、メリットは以下の公式ドキュメントを読んでもらえればと思います。

viewcomponent.org

個人的に導入、活用していて、「Single responsibility(単一責任)」「Testing(テスト)」あたりの見通しの良さは、これまでのRailsにはなかったように感じます。

開発も精力的に行われているため、目立ったバグなどは今のところなさそうです。またパフォーマンス自体も通常のpartialよりも早いと謳われており、その点もそこまで気にせずに使えるのもメリットかと思います。

さて、ViewComponent、ViewComponent::Storybookの導入ですが、gemなので、Gemfileに以下を追加します。

gem 'view_component', require: 'view_component/engine'

group :development do
  gem 'view_component_storybook'
end

Storybookを本番で使わない場合は、group以下にしておくとよいかと思います。

あとはgemをインストールしておきます。

$ bundle install

最後に、必要なnpmパッケージを追加します。今回はyarnを利用しています。

$ yarn add @storybook/server @storybook/addon-controls --dev

ViewComponentを作成する

まずはViewComponentを作成します。

今回は preview機能を使いたいため、previewオプションを付けて作成します。

$ bundle exec rails generate component Button type --preview

これにより、以下の4つのファイルが作成されます。 button_component_preview.rb--preview をつけていないと作成されません。

app/components/button_component.html.erb
app/components/button_component.rb 
test/components/button_component_test.rb
test/components/previews/button_component_preview.rb

コンポーネントの実装

コンポーネントを実装していきます。

# frozen_string_literal: true

class ButtonComponent < ViewComponent::Base
  def initialize(type: :primary, large: false)
    @type = type
    @large = large
  end

  def classes
    cls = ['btn', "btn-#{@type}"]
    cls += ['btn-lg'] if @large
    cls.join(' ')
  end
end
<button class="<%= classes %>">
  <%= content %>
</button>

プレビューの実装

ViewComponentにはプレビュー機能が実装されており、プレビューのクラスを実装した上で、特定のパスにアクセスするとそのコンポーネントの表示を確認することができます。

# frozen_string_literal: true

class ButtonComponentPreview < ViewComponent::Preview
  def default(type: :primary, large: false)
    type = type.to_sym if type
    large = large == 'true'

    render(ButtonComponent.new(type: type, large: large)) { type.to_s }
  end
end

この状態で、 bundle exec rails s などでサーバーを起動して、 /rails/view_components/button_component/default?type=primary&large=true にアクセスすると以下のようにコンポーネントの表示を確認することができます。

Storybookで表示できるようにする

ViewComponentのプレビュー機能で、特定のパスにアクセスすることでコンポーネントを表示させることができるようになりました。

ここから、ViewComponent::Storybook でStorybookを表示します。

ViewComponent:Storybook を使えるように準備する

ViewComponent::Storybook を使うためにはもう少し準備が必要です。

まずは、以下を config/application.rb に追加します。

module RailsViewComponentsStorybook
  class Application < Rails::Application
    config.load_defaults 6.1
    require "view_component/storybook/engine"
    config.view_component.show_previews = true
  end
end

次に、 .storybook/main.js.storybook/preview.js でStorybook自体の構成を設定しておきます。

module.exports = {
  stories: ["../test/components/**/*.stories.json"],
  addons: ["@storybook/addon-controls"],
};
export const parameters = {
    server: {
        url: `${location.protocol}${location.hostname}${location.port !== "" ? ":3000" : ""}/rails/view_components`,
    },
}

ここでサーバーを再起動して、 /_storybook/index.html にアクセスすれば、Storybook自体にはアクセスできます。

Storyを作成する

この時点では、Storyを作成していないので、何も表示されません。

そのため、作成したコンポーネントをStoryで表示できるようにします。

class ButtonComponentStories < ViewComponent::Storybook::Stories
  title 'ボタン'

  story(:default) do
    constructor(type: select(%i[primary secondary success danger warning info light dark link], :primary), large: boolean(false))
  end
end

ViewComponent:Storybook でのStoryの書き方は以下の公式ドキュメントを参考にしてください。
jonspalmer.github.io

このままではまだ表示できません。

このRubyクラスから、Storybookのjsonファイルを作成して、そのjsonファイルから、Storybookの最終的なアセットを出力します。

$ bundle exec rake view_component_storybook:write_stories_json
$ yarn build-storybook -o public/_storybook

これで /_storybook/index.html にアクセスしてStorybookを表示すると、ボタンのStoryが追加されています。

Rakeタスクにして、一発で出力する

Storyの変更があるたびに、コマンドを2回叩くのは面倒なので、Rakeタスクにしてしまいます。

namespace :storybook do
  desc 'rebuild storybook'
  task rebuild: :environment do
    Rake::Task['view_component_storybook:write_stories_json'].invoke
    sh 'yarn build-storybook -o public/_storybook'
  end
end

まとめ

Ruby on Rails 単体でSSRメインで開発していると、なかなかStorybookを導入しにくいと感じていましたが、ViewComponentを利用している場合は、比較的簡単に導入することができました。

デザイナーなどとの協業の場合にも、どのようなコンポーネントのデザインになるかを簡単に動く形で共有できるようになるので、便利そうですね。