In Praise of the Current Pattern
Matthew • December 15, 2022
The "current pattern" or "current context" is a technique that allows you to access some per-request data easily without having to explicitly pass it around everywhere. Recent versions of Rails offer explicit support for it. The current pattern, used judiciously, will help clean up your code and make your life swell!
What is the current pattern?
The current pattern is a singleton class instance you can access anywhere within a web request cycle. You typically initialize this instance at the start of the request and, from there on, read these values.
When you use it in Ruby on Rails, instead of having to pass, for example, a
user variable for the current user as a parameter to every method in the entire call stack, instead, you can do this:
if Current.user.manager? # Do manager-only stuff... end
How do I implement the current pattern?
As of Rails 5.2.3, you can use ActiveSupport::CurrentAttributes to implement the pattern.
Before that, you will have to do more work yourself. I'm (unhelpfully!) not going to cover how to do that here except to say that you're going to want to make it threadsafe. (If uncomfortable doing that, spend the time upgrading to a newer Rails version instead!) (Update: Several folks have mentioned that the RequestStore gem provides some of the features and is available in earlier versions of Rails.)
ActiveSupport::CurrentAttributes provides a threadsafe store which is reset at the start of every request cycle. It also offers other helpful features, so be sure to read the documentation.
How do I use the current pattern?
Here's an example from one project where I've employed this pattern.
You'll notice that this looks (very!) similar to the example in the Rails documentation. And that's not a surprise given that this pattern is, well, a pattern!
Here, I'm grabbing the user, if it's present, out of Devise after performing authentication.
class Current < ActiveSupport::CurrentAttributes attribute :account attribute :user resets do Time.zone = nil end def user=(user) super self.account = user.account Time.zone = user.time_zone end end class ApplicationController < ActionController::Base before_action :authenticate_user! before_action :set_current_user def set_current_user # current_user comes from Devise Current.user = current_user || GuestUser.new end end
Current.account are available to me anywhere in my request processing.
Sir, you've described a global variable
Some people might be tempted to call this a global variable, and those people would be absolutely right!
In general, we're all down on global state, but this technique provides more value than pain.
First, yes, it is global state, but just little bit of global state! I suggest you rethink your usage if you have, say, more than ten or so attributes in the current context. If an attributes is not something you're using in just about every request, it probably doesn't belong here.
Second, it's my experience that the values in the current context are usually the same cast of characters: the current user, the current user's time zone, the current account, the current team, and so on. And that's about it for me, usually. So, you have a pretty good idea about when you're likely working with data that is global to the thread.
Third, this state is short-lived, primarily read-only, and consumed only within the context of a single request. So, this further constrains the potential negative consequences.
The combination of these factors weighed against its utility makes it pretty palatable.
It can make testing more painful!
One of the most significant downsides I've experienced using this pattern is that you'll sometime need to set up your instance of
Current in some of your tests. I find that off-putting and annoying.
At those times, you'll definitely be asking yourself, "why am I using this stupid global variable?"
The technique I've used to mitigate this is to, where possible, use the
Current value to initialize a default method parameter instead of accessing it directly within the method's body. (So, dependency injection.)
So, instead of this:
class Reservation def editable? Current.user.editor? && recently_created? end end
class Reservation def editable?(user: Current.user) user.editor? && recently_created? end end
I've been satisfied with this approach.
Thanks for reading! I hope you find this useful and that it helps you write some more concise code. Did you find this useful? Let me know! You can find me on Post.news at @mjbellantoni on post.news, the Ruby Job Board page on LinkedIn, or Reddit.