DRY Off Your Rails Code with ActiveSupport::Concerns

Share this article

DRY Off Your Rails Code with ActiveSupport::Concerns
hair dryer  icon

If you have been building Rails applications for a while, you have likely noticed a folder called concerns. This folder gets created inside the app/controllers and app/models directories whenever you generate a new Rails application. I thought it was useless until recently when we had to make use of it at work.

In this short tutorial, I want to show you how to harness the power of concerns.

We are going to build a mini tweeting application in Rails, I will call it Twik. In the application, we will have two TwitsController. One controller will be namespaced under admin (so it’s actions are accessible to just admins), and the other will be in the conventional Rails namespace. We will share functionality using ActiveSuppourt::Concern to make sure we abide by the DRY principle.

After seeing how it works for controllers, I’ll show you how to use ActiveSupport::Concern in models.

Let’s get started.

Application Setup

Generate your Rails application:

rails new twik -T

Add the following gems to your Gemfile:

...
gem 'devise'
gem 'bootstrap-sass'

Now bundle install your gems.

Run the command to install Devise:

rails g devise:install

Now let’s generate our Admin model:

rails g devise Admin

Rename app/assets/stylesheets/application.css to app/assets/stylesheets/application.scss and paste in the following:

#app/assets/stylesheets/application.scss

@import "bootstrap-sprockets";
@import "bootstrap";

Navigate to app/assets/javascripts/application.js and add the line above the last require:

...
//= require bootstrap-sprockets (ADD THIS)
//= require_tree .

Create the file app/views/layouts/_navigation.html.erb, and paste in the following:

#app/views/layouts/_navigation.html.erb

<nav class="navbar navbar-default">
  <div class="container-fluid">
    <!-- Brand and toggle get grouped for better mobile display -->
    <div class="navbar-header">
      <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar-collapse" aria-expanded="false">
        <span class="sr-only">Toggle navigation</span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </button>
      <% if admin_signed_in? %>
        <%= link_to "Twik", admin_twits_path, class: "navbar-brand" %>
      <% else %>
        <%= link_to "Twik", root_path, class: "navbar-brand" %>
      <% end %>
    </div>
    <div class="collapse navbar-collapse" id="navbar-collapse">
      <ul class="nav navbar-nav navbar-right">
        <li><%= link_to 'Home', root_path %></li>
        <% if admin_signed_in? %>
          <li><%= link_to 'My Account', edit_admin_registration_path  %></li>
          <li><%= link_to 'Logout', destroy_admin_session_path, :method => :delete %></li>
        <% else %>
          <li><%= link_to 'Login', new_admin_session_path  %></li>
        <% end %>
      </ul>
    </div>
  </div>
</nav>

Now edit app/views/layouts/application.html.erb to look like this:

#app/views/layouts/application.html.erb

<!DOCTYPE html>
<html>
  <head>
    <title>Twik</title>
    <%= csrf_meta_tags %>

    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <%= render "layouts/navigation" %>
    <div class="container-fluid">
      <%= yield %>
    </div>
  </body>
</html>

Twits Controllers and Concerns.

We want to write as little code as possible, ensuring that we do not repeat things. To achieve this we will use concerns to share actions in both controllers. Confused? Do the following:

  • Generate your Twit model: rails g model Twit tweet:text.
  • Generate your TwitsController: rails g controller TwitsController.
  • Navigate to app/controllers/concerns and create the file twitable.rb, pasting in the following:
  #app/controllers/concerns/twitable.rb

  module Twitable
    extend ActiveSupport::Concern

    included do
      before_action :set_twit, only: [:show, :edit, :destroy, :update]
    end

    def index
      @twits = Twit.all
    end

    def new
      @twit = Twit.new
    end

    def show
    end

    def create
      @twit = Twit.new(twit_params)
      if @twit.save
        flash[:notice] = "Successfully created twit."
        redirect_to @twit
      else
        flash[:alert] = "Error creating twit."
        render :new
      end
    end

    private

    def twit_params
      params.require(:twit).permit(:tweet)
    end

    def set_twit
      @twit = Twit.find(params[:id])
    end
  end

Now, let’s examine the code above.

Using extend ActiveSupport::Concern tells Rails that we are creating a concern. The code within the included block will be executed wherever the module is included. This is best for including third party functionality. In this case, we will get an error if the before_action is written outside of the included block. At this point, we are good to include our Twitable module to the controllers that need this behavior.

With that done, our TwitsController is very concise:

#app/controllers/twits_controllers.rb

class TwitsController < ApplicationController
  include Twitable
end

We need an admin directory to house the controller for admins:

mkdir app/controllers/admin
touch app/controllers/admin/twits_controllers.rb

Now paste this code into the file you just created:

#app/controllers/admin/twits_controllers.rb

class Admin::TwitsController < ApplicationController
  include Twitable

  def edit
  end

  def update
    if @twit.update_attributes(twit_params)
      flash[:notice] = "Successfully updated twit."
      redirect_to admin_twit_path
    else
      flash[:alert] = "Error creating twit."
      render :edit
    end
  end

  def destroy
    if @twit.destroy
      flash[:notice] = "Successfully deleted twit."
      redirect_to twits_path
    else
      flash[:alert] = "Error deleting twit."
    end
  end
end

Is that not cool? We do not have to repeat our code. Now, let’s create the views, so we can test if all of these really works:

mkdir -p app/views/admin/twits
touch app/views/admin/twits/index.html.erb
touch app/views/admin/twits/new.html.erb
touch app/views/admin/twits/show.html.erb
touch app/views/admin/twits/edit.html.erb
touch app/views/twits/new.html.erb
touch app/views/twits/show.html.erb
touch app/views/twits/index.html.erb

Paste the code below in the respective files. Admin Twit Edit Page

#app/views/admin/twits/edit.html.erb

<div class="container-fluid">
  <div class="row">
    <div class="col-sm-offset-4 col-sm-4 col-xs-12">
      <%= form_for @twit, :url => {:controller => "twits", :action => "update" } do |f| %>
        <div class="form-group">
          <%= f.label :tweet %>
          <%= f.text_field :tweet, class: "form-control" %>
        </div>
        <div class="form-group">
          <%= f.submit "Update", class: "btn btn-primary" %>
          <%= link_to "Cancel", :back, class: "btn btn-default" %>
        </div>
      <% end %>
    </div>
  </div>
</div>

Admin Twit Index Page

#app/views/admin/twits/index.html.erb

<div class="container-fluid">
  <p id="notice"><%= notice %></p>

  <h1>Listing Twits</h1>

  <div class="row">
  <div class="col-sm-12 col-xs-12">
    <%= link_to "New Tweet", new_admin_twit_path, class: "btn btn-primary pull-right" %>
  </div>
  </div>
  <div class="row">
    <div class="col-sm-12 col-xs-12">
      <div class="table-responsive">
        <table class="table table-striped table-bordered table-hover">
          <tbody>
            <% @twits.each do |twit| %>
              <tr>
                <td class="col-sm-8 col-xs-8"><%= twit.tweet %></td>
                <td class="col-sm-4 col-xs-4"><%= link_to 'Show', admin_twit_path(twit), class: "btn btn-primary" %>
                    <%= link_to 'Edit', edit_admin_twit_path(twit), class: "btn btn-default" %>
                    <%= link_to "Delete", admin_twit_path(twit), class: "btn btn-danger", data: {:confirm => "Are you sure?"}, method: :delete %>
                </td>
              </tr>
            <% end %>
          </tbody>
        </table>
      </div>
    </div>
  </div>
</div>

Admin New Twit Page

#app/views/admin/twits/new.html.erb

<div class="container-fluid">
  <div class="row">
    <div class="col-sm-offset-4 col-sm-4 col-xs-12">
      <%= form_for @twit do |f| %>
        <div class="form-group">
          <%= f.label :tweet %>
          <%= f.text_field :tweet, class: "form-control" %>
        </div>
        <div class="form-group">
          <%= f.submit "Submit", class: "btn btn-primary" %>
          <%= link_to "Cancel", :back, class: "btn btn-default" %>
        </div>
      <% end %>
    </div>
  </div>
</div>

Admin Twit Show Page

#app/views/admin/twits/show.html.erb

<div>
  <h2><%= @twit.tweet %></h2>
</div>

Twit Index Page

#app/views/twits/index.html.erb

<div class="container-fluid">
  <p id="notice"><%= notice %></p>

  <h1>Listing Twits</h1>

  <div class="row">
  <div class="col-sm-12 col-xs-12">
    <%= link_to "New Tweet", new_twit_path, class: "btn btn-primary pull-right" %>
  </div>
  </div>
  <div class="row">
    <div class="col-sm-12 col-xs-12">
      <div class="table-responsive">
        <table class="table table-striped table-bordered table-hover">
          <tbody>
            <% @twits.each do |twit| %>
              <tr>
                <td><%= twit.tweet %></td>
                <td><%= link_to 'Show', twit, class: "btn btn-primary" %></td>
              </tr>
            <% end %>
          </tbody>
        </table>
      </div>
    </div>
  </div>
</div>

New Twit Page

#app/views/twits/new.html.erb

<div class="container-fluid">
  <div class="row">
    <div class="col-sm-offset-4 col-sm-4 col-xs-12">
      <%= form_for @twit do |f| %>
        <div class="form-group">
          <%= f.label :tweet %>
          <%= f.text_field :tweet, class: "form-control" %>
        </div>
        <div class="form-group">
          <%= f.submit "Submit", class: "btn btn-primary" %>
          <%= link_to "Cancel", :back, class: "btn btn-default" %>
        </div>
      <% end %>
    </div>
  </div>
</div>

Twit Show Page

#app/views/twits/show.html.erb

<div>
  <h2><%= @twit.tweet %></h2>
</div>

Now start up your Rails server by running rails server. Navigate through your website and you’ll see that everything works fine. Your codebase is neat and your controllers are thin, thanks to concern.

Concerns in Models

ActiveSupport::Concern works in Rails models, just like we saw in controllers. If you grasp what we did above you will be able to implement it in your models where necessary. Let’s say we have a reply feature in our application. With this feature, users can reply to twits (duh). Alongside the reply feature, we have a voting feature allowing users to vote on twits and replies. So, now we have three models: Twit, Reply, and Vote.

Twit and Reply have many votes, so their models look like this:

class Twit < ActiveRecord::Base
  has_many :votes, as: :votable
  has_many :replies

  def vote!
    votes.create
  end
end

class Reply < ActiveRecord::Base
  has_many :votes, as: :votable
  belongs_to :twits

  def vote!
    votes.create
  end
end

class Vote < ActiveRecord::Base
 belongs_to :votable, polymorphic: true
end

Using concerns, you can make things look pretty and neat. Here is how you might want to do it:

module Votable
  extend ActiveSupport::Concern

 included do
  has_many :votes, as: :votable
 end

 def vote!
  votes.create
 end
end

class Twit < ActiveRecord::Base
  include Votable
  has_many :replies
end

class Reply < ActiveRecord::Base
  include Votable
  belongs_to :twit
end

I am sure you will agree with me that this path is much better.

Conclusion

The goal of this tutorial is simple. I simply wanted to show you a way to abide by the DRY principle using ActiveSupport::Concern. I hope it was worth the time :)

Frequently Asked Questions (FAQs) about ActiveSupport::Concerns in Rails

What is the main purpose of ActiveSupport::Concerns in Rails?

ActiveSupport::Concerns in Rails is a module that provides a simple and clean way to encapsulate a shared piece of code across multiple classes. It is designed to make it easier to declare class-level methods, instance methods, and even hooks in a module, which can then be included in any class. This helps in keeping your code DRY (Don’t Repeat Yourself), organized, and easy to maintain.

How does ActiveSupport::Concerns differ from traditional Ruby modules?

While traditional Ruby modules allow you to define methods that can be mixed into classes, ActiveSupport::Concerns takes it a step further by providing a structure for defining both instance and class methods, as well as other module-level aspects, such as dependencies and hooks. This makes it easier to share complex code structures between classes.

How do I define a class method within a concern?

In ActiveSupport::Concerns, class methods are defined within a block called class_methods. Here’s an example:

module Reusable
extend ActiveSupport::Concern

class_methods do
def some_class_method
# method body
end
end
end

Can I use hooks within a concern?

Yes, ActiveSupport::Concerns allows you to use hooks within a concern. These hooks will be executed in the context of the class that includes the concern. For example:

module Reusable
extend ActiveSupport::Concern

included do
before_save :do_something
end

def do_something
# method body
end
end

How do I include a concern in a class?

To include a concern in a class, you simply use the include keyword followed by the name of the concern. For example:

class MyClass < ApplicationRecord
include Reusable
end

Can I include multiple concerns in a single class?

Yes, you can include multiple concerns in a single class. Each concern should be included with a separate include statement.

Can concerns depend on each other?

Yes, concerns can depend on each other. If a concern depends on methods defined in another concern, you can use the requires keyword to specify this dependency.

Can I use concerns in controllers?

Yes, concerns can be used in both models and controllers. This allows you to share code between different parts of your application.

Can concerns be tested?

Yes, concerns can and should be tested. You can write unit tests for your concerns just like you would for any other piece of code.

Are there any drawbacks to using concerns?

While concerns can help keep your code organized and DRY, they can also lead to issues if not used carefully. For example, they can make it harder to understand the code if they are overused or if the relationship between the concern and the class is not clear. It’s important to use concerns judiciously and to keep them focused and small.

Kingsley SilasKingsley Silas
View Author

Kingsley Silas is a web developer from Nigeria. He has a hunger for acquiring new knowledge in every aspect he finds interesting.

DRYGlennGRuby on Rails
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week