React Context pattern in Rails

thevtm

Vinícius Manjabosco

Posted on November 26, 2020

React Context pattern in Rails

I've recently found myself passing a lot of parameters down from controllers to service objects and then to jobs, etc.

This was one of the problems that were solved by the context pattern in React so I tried to do the same in the Rails app that I've been working on.

I had seen something a bit similar to in the I18n.with_locale function.

So I wrote this:

# frozen_string_literal: true

require "concurrent-ruby"

class RequestValueContext
  class << self
    # For the multi threaded environment
    @@request_value = Concurrent::ThreadLocalVar.new

    def with(request_value)
      if get.present?
        raise ContextAlreadyDefinedError,
          "Context already defined!"
      end

      begin
        @@request_value.value = request_value
        yield
      ensure
        @@request_value.value = nil
      end
    end

    def get
      @@request_value.value
    end
  end

  ContextAlreadyDefinedError = Class.new(StandardError)
end
Enter fullscreen mode Exit fullscreen mode

And in the ApplicationController I've added this:

class ApplicationController < ActionController::Base
  around_action :with_context

  def with_context
    RequestValueContext.with("foo") do
      yield
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Then I can access the value using RequestValueContext.get from any method that is called "within the controller stack".

A nice feature of this pattern is that the current context can be captured when the using ActiveJob::Callbacks.before_enqueue and then provided by ActiveJob::Callbacks.around_perform like so:

# frozen_string_literal: true

module WithContexts
  extend ActiveSupport::Concern

  REQUEST_VALUE_KEY = "request_value"

  included do
    attr_reader :request_value, :deserialize_called

    before_enqueue :capture_context
    around_perform :provide_context
  end

  def serialize
    super.merge(REQUEST_VALUE_KEY => request_value)
  end

  def deserialize(job_data)
    # "detects" when a job is called by *perform_now*
    @deserialize_called = true

    super

    @doorkeeper_application = request_value
  end

  def capture_context
    @doorkeeper_application = RequestValueContext.get
  end

  def provide_context
    if job_called_by_perform_now?
      # if the job is called by *perform_now* it will be executed inline
      # with the current context
      yield
    else
      RequestValueContext.with_application(request_value) do
        yield
      end
    end
  end

  def job_called_by_perform_now?
    !deserialize_called
  end
end
Enter fullscreen mode Exit fullscreen mode

I believe something similar could be done for Proc/Block/Lambda.

I started writing Ruby less than a year ago and I found it to be quite a tricky language so if you have any feedback please let me know.

💖 💪 🙅 🚩
thevtm
Vinícius Manjabosco

Posted on November 26, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

React Context pattern in Rails
ruby React Context pattern in Rails

November 26, 2020