Current attributes in Rails
Published in February 2020
Overview of CurrentAttributes
Active Support CurrentAttributes adds global per-request attributes to your Rails app. To get started, create a Current class inside your models folder. You can assign any model in your application as an attribute to use as a global variable. We will discuss it later, but you should only use CurrentAttributes for your most important and widely used models within your application, for example, Project or Team.
Check out the commit and pull request created by DHH to learn more about how CurrentAttributes work under the hood. You don't have to understand how the entire code works, but I find it useful to see an overview of how the "Rails magic" is created.
In the example below, I have assigned my User model to be a CurrentAttribute.
# app/models/current.rb class Current < ActiveSupport::CurrentAttributes attribute :user end
You can then set your CurrentAttributes simply by assigning them to an object
Current.user = current_user
Once you set this, you will be able to access your user object globally (for example, within views and models) without the need to declare them all the time.
Example: create a subdomain for your projects and use that subdomain to assign it to CurrentAttributes
Best way to learn about CurrentAttributes is to create a quick example illustrating a use case for it. For our example, we want to have projects names/slugs as our subdomain. For example project-name.lvh.me:3000.
Create a Project with a name, subdomain and a user_id fields. Users have many projects and a project belongs to a user.
In order to complete this example, let's sort out the subdomains. This is not a tutorial on subdomains, so the code is limited to just showcase CurrentAttributes.
# routes.rb require "subdomain_required" Rails.application.routes.draw do #... constraints(SubdomainRequired) do resources :projects # root root "projects#show", as: :projects_root end # root to: 'home#index' end
# lib/subdomain_required.rb class SubdomainRequired def self.matches?(request) request.subdomain.present? && request.subdomain != 'www' end end
To share a devise user session across subdomains:
# initializer/session_store.rb # replace current_attributes_example with your app name Rails.application.config.session_store :cookie_store, key: '_current_attributes_example_session'
Create the Current class as the example above and assign project as the attribute.
# app/models/current.rb class Current < ActiveSupport::CurrentAttributes attribute :project end
Then we can create a before_action in the ApplicationController where we assign Current.project to the project depending on the subdomain.
before_action :set_project private def set_project Current.project = current_user.projects.find_by_subdomain(request.subdomain) if request.subdomain.present? end
You can then use Current.project within our application and this will be scoped based on our subdomain.
You can then list your user projects and link them to the subdomain
<% current_user.projects.each do |project| %> <%= link_to project.name, projects_roots_url(subdomain: project.subdomain) %> <% end %>
You will get redirected to the subdomain and our Current.project will have our relevant project.
<h1>Welcome to <%= Current.project.name %></h1>
Our project is now present globally within our app so we can continue writing our logic using Current.project instead of querying the project in every controller, model and view.
Beware of potential problems when using CurrentAttributes
I came across this article which warns of the potential dangers of using CurrentAttributes and global state in general.
Important note it raises that you should be aware of - avoid using CurrentAttributes in background jobs. Instead, pass your CurrentAttributes object's id as a parameter to your background job and then find the record within. This makes your background jobs explicit and avoids the danger of using the wrong record to perform the job.
# don't pass the object itself def perform(user) # user is Current.user end # pass the id of Current.user instead def perform(user_id) user = User.find user_id end
DHH recommends using CurrentAttributes for a few, top-level globals, like account and user. Consider using CurrentAttributes if you are going to use this attribute in most of your actions. Otherwise, CurrentAttributes will become anti-productive and introduce problems within your application.
In conclusion, CurrentAttribute can be a great help to your application, but you should use it with caution and you should be aware of the dangers of using a global variable.
Level up your web development skills
Get articles, guides and interviews right in your inbox. Join a community of fellow developers.
No spam. Unsubscribe at any time.