inertiajs / inertia-rails

The Rails adapter for Inertia.js.

Home Page:https://inertiajs.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Rethinking Shared Data

bknoles opened this issue · comments

Continuing the conversation from #108

A quick summary:

  • The combination of "module level" shared data storage, and "controller level" setters for shared data has introduced tricky bugs including shared data leaking across threads and across requests.
  • While we've patched these issues, it'd be great to refactor away from global shared state.
  • Controller class variables have been suggested in the past.
  • I'm wary of using controller class variables, since in production they are also a form of global shared state.
  • @PedroAugustoRamalhoDuarte has put together #111 , which refactors inertia shared state into instance variables. These are set via before_action callbacks, called by inertia_share.

Ok, so here are a couple examples of failing tests:

First, #117 demonstrates that class_attribute will leak data in the case where inertia_share is called conditionally. Here's the commit with the failing example.

The situation changes a bit with the refactor to instance variables in #116. You can still get leaky data, but it's more awkward to get there. Here's the commit with the failing test. Because of the refactor to before_actions, you actually have to call the route with conditional sharing twice in order to get the error. The shared data wouldn't ever be present on the first page load in development, so I'd guess this wouldn't make it into a production application.

Both of these are contrived examples that are "incorrect" uses of Inertia. But I think it's reasonable for a developer to expect to share data on a per-action basis, instead of per-controller. Especially since other Rails controller class methods like before_action allow per-action configuration. I think we should support it in the gem.

That said, I don't think we need to solve that in order to approve #111. It replaces global storage with the per-request storage of instance variables. That removes the necessity for a lot of current workarounds without changing the public API at all. As the PR notes, conditional sharing can be a future improvement.

Here a couple (untested) sketches for API changes that could support conditional rendering. Essentially just passing options to the before_action within inertia_share.

The first one requires a breaking change. Plain data props must be defined in a props: argument. Everything else gets passed onto before_action. It's the same API as the renderer, which is nice, but breaking changes are no fun.

module InertiaRails
  module Controller
    extend ActiveSupport::Concern

    module ClassMethods
      def inertia_share(props: nil, **options, &block)
        before_action(**options) do
          @_inertia_shared_plain_data = @_inertia_shared_plain_data.merge(props) if props
          @_inertia_shared_blocks = @_inertia_shared_blocks + [block] if block_given?
        end
      end
    end
  end
end

class SomeController < ApplicationController
  before_action props: { first_name: 'Brian' }, only: [:show]
  before_action only: [:show] do
    {
      last_name: 'Knoles'
    }
  end
end

And here's one where we configure the before_action via a before_action_options: argument. It's not as elegant, but it wouldn't be a breaking change.

module InertiaRails
  module Controller
    extend ActiveSupport::Concern

    module ClassMethods
      def inertia_share(before_action_options: {}, **props, &block)
        before_action(**before_action_options) do
          @_inertia_shared_plain_data = @_inertia_shared_plain_data.merge(props) if props
          @_inertia_shared_blocks = @_inertia_shared_blocks + [block] if block_given?
        end
      end
    end
  end
end

class SomeController < ApplicationController
  before_action first_name: 'Brian', before_action_options: { only: [:show] }
  before_action before_action_options: { only: [:show] } do
    {
      last_name: 'Knoles'
    }
  end
end

@bknoles Thanks for the time invested in this refactor.

  • The syntax of the first solution is more elegant to me, but I think it not worth the breaking change.
  • We can go with the second one, maybe we can think in a smaller or better name for before_action_options, maybe: share_options, run_options