Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add inertia_config to enable controller-specific config #121

Merged
merged 25 commits into from
Oct 21, 2024

Conversation

ElMassimo
Copy link
Contributor

@ElMassimo ElMassimo commented Jun 26, 2024

Description 📖

This pull request is a complete refactor of the implementation in order to introduce controller-specific configuration.

inertia_config

A new way for controllers to override the global configuration, and provide dynamic values, such as:

class PostsController < ApplicationController
  inertia_config(
    ssr_enabled: -> { action_name == "index" },
  )
end

Just like inertia_share, configuration is inheritable, and subclasses can override values.

In addition, all configuration options can now be procs (before only version supported this).

Performance 🚀

The implementation of inertia_share no longer relies on before_action, improving performance when calculating shared data at runtime, and reducing object allocations.

As an additional benefit, we no longer run this code unnecessarily for non-Inertia requests.

Notes ✏️

Each commit is a gradual refactoring, and can be read in isolation (and passes all tests). I'd still suggest using Squash and Merge.

We also freeze config and shared data to prevent any mutations, ensuring it's safe for multiple threads to access the configuration from the controller class.

def saved_version
InertiaRails.version.is_a?(Numeric) ? InertiaRails.version.to_f : InertiaRails.version
def coerce_version(version)
server_version.is_a?(Numeric) ? version.to_f : version
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is that a typo?

Suggested change
server_version.is_a?(Numeric) ? version.to_f : version
version.is_a?(Numeric) ? version.to_f : version

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, that's the original logic.

default_render: false,

# Let Rails decide which layout should be used based on the controller configuration.
layout: true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that's a breaking change, since current default is nil.
We can write about that in the changelog, or add a default to Renderer#layout:

     def layout
       # we can also raise a warning here?
       return true if configuration.layout.nil?

       configuration.layout
     end

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made the original change in #86.

The controller method was returning true if there global was nil, so the behavior stays the same.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just tested, if someone sets nil manually, this PR will brake their code (Rails renders page without layout):

InertiaRails.configure do |config|
  config.layout = nil
end

Copy link
Contributor Author

@ElMassimo ElMassimo Jun 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unlikely that users are doing that, given it's the default.

That behavior would also be consistent with using layout: false.

If this is released in a major version, I'd say it's more intuitive if whatever the user configures is not overriden internally.

As an alternative, we can raise if the user sets it to nil, since it's more likely a mistake.

Finally, I'd suggest removing layout in the future, it makes more sense to customize using plain Rails in controllers instead, although I'm not proposing we do this now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup, that's a great addition, and since README uses true as a default, a note in the changelog should be enough, I guess 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't feel strongly about it, we could be strict in backwards compatibility first, and remove it later, just mentioning that I don't think it's as useful a setting after #86.

Thanks for the review 😃

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've re-introduced the layout.nil? ? true : layout check, in order to minimize friction when upgrading. Thanks!

@@ -1,3 +1,3 @@
<%= inertia_headers %>
<%= local_assigns[:inertia_ssr_head] %>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's wrap this in inertia_headers helper for backwards compatibility?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed 👍

I would add it with a warning that it will be removed.

These are not headers, it's html for the head, so the name is inaccurate.

In addition, it does not make sense to call this in the controller, as it wouldn't have been set yet.

@ElMassimo ElMassimo changed the title refactor: remove all global state while maintaining backwards compatibility feat: add inertia_config to enable controller-specific config Jun 26, 2024
Shared data is now frozen to prevent

Also, match the method names for instance variables.

Updated a test that used to call `inertia_share` inside a `before_action`
as that seems like something a user should never do.
Copy link
Contributor

@PedroAugustoRamalhoDuarte PedroAugustoRamalhoDuarte left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ElMassimo Excellent PR, very important to make easy to set up Inertia V2. This #131 task could be easily resolved if this PR is merged

@@ -54,63 +54,6 @@
it { is_expected.to eq props.merge({ errors: errors }) }
end

context 'multithreaded inertia share' do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ElMassimo just for curiosity, why do you remove this test? It was failing or because we do not have this problem anymore by default?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not possible to have multi-threading issues anymore, as the objects that can be shared between threads are now frozen (not writable, and thus not susceptible to concurrent writes either).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the response!!

Copy link
Collaborator

@bknoles bknoles Oct 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fwiw, i pulled the branch and added these tests back (just out of curiosity), and they do still pass.

These were made unnecessary by #111. At the time, I asked @PedroAugustoRamalhoDuarte to keep the specs so we weren't changing spec + implementation at the same time. I'm good with removing them now that we have evidence that refactor passed the original spec. (kind of like adding a new DB column, migrating data, then removing the old column once everything is verified working in production instead of just renaming the column in one shot)

def inertia_layout
layout = ::InertiaRails.layout
def inertia_shared_data
initial_data = session[:inertia_errors].present? ? {errors: session[:inertia_errors]} : {}
Copy link
Contributor

@skryukov skryukov Oct 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used to set inertia_share(errors: {}) to mimic Laravel plugin's default, with this change — I always overwrite inertia's errors with my empty hash.

Should we move this to the end of the chain?

    def inertia_shared_data
      self.class._inertia_shared_data.filter_map { |shared_data|
        if shared_data.respond_to?(:call)
          instance_exec(&shared_data)
        else
          shared_data
        end
      }.reduce({}, &:merge).tap do |data|
        data[:errors] = session[:inertia_errors] if session[:inertia_errors].present?
      end
    end

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think what you are describing is only working by accident. Before this change, I believe inertia_share(errors: {}) would have overwritten the errors from the session (exactly as you described). That change moved the error sharing into a block, which gets evaluated after the plain data is computed, allowing your technique to work. I'd call that unintended behavior because presumably this equivalent looking code just wouldn't work:

# This will always overwrite the session errors because this block will be evaluated after the error sharing block from the library
inertia_share do
  {errors: {}}
end

IMO, if the application code explicitly sets a prop it should overwrite whatever came out of the library internals, so this PR is actually fixing that.

That said, maybe our default should match the Laravel default and always return something in an errors prop? That seems to be the real thing at issue here. I don't have a strong opinion, but I'd lean towards keeping the default we have since it'd be a potentially breaking change.

I consider that separate from the scope of this PR and won't hold up merging it for this.

(Please let me know if I'm misunderstanding your use case)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, great, I just noticed a breaking change, although it appears to be a fix 😂.

Regarding errors: {} being the default—that's a significant breaking change, and I would suggest adding a compatibility configuration flag for opting into backward compatibility until the next major release. I also think we should first discuss the possibility of adding this behavior to the protocol. But anyway, that's a separate issue 👍

before_action do
@_inertia_shared_plain_data = @_inertia_shared_plain_data.merge(hash) if hash
@_inertia_shared_blocks = @_inertia_shared_blocks + [block] if block_given?
def inertia_share(**attrs, &block)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breaking change, should we continue supporting hashes or at least throw a deprecation at first, wdyt?

inertia_share({foo: 'bar'}) # => throws error

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in #137

@bknoles
Copy link
Collaborator

bknoles commented Oct 15, 2024

Hey @ElMassimo thanks for this PR. I'm digging into it now (at long last)!

Just as a heads up, I resolved a merge conflict in the README and committed it to your branch. Hope that's not stepping on your toes.

```ruby
class EventsController < ApplicationController
inertia_config(
version: "events-#{InertiaRails.configuration.version}",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Local version configuration seems like a bit of a footgun. In this example, won't you trigger a hard refresh every time you visit this controller from a different controller since those versions will always be mismatched?

All the other configuration options make perfect sense to optionally configure per-controller. Maybe we should exclude version (and only version) from being locally configured?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

won't you trigger a hard refresh every time you visit this controller from a different controller since those versions will always be mismatched?

You could have different apps, with entirely different Vite bundles, so that would actually make sense and the hard refresh would "make it work". Linking across apps using Inertia navigations isn't "recommended", but it's nice that it would work out of the box.

Having an exception for version seems unnecessary, it's completely optional to use it, and in some cases it's actually useful.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah interesting. So, to make sure I understand with a real life example, you might have a "storefront", with one JS bundle, and an "admin interface" with a separate bundle.

And your StorefrontBaseController and AdminBaseController would configure different versions. And in that scenario, if you Inertia-navigate from /storefront to /admin, a hard refresh isn't a problem.

One could create a mess by locally versioning controllers that target the same JS bundle (as I descriebd), but we are Rails developers and we like our knives sharp :)

I'm on board now. Thanks!

@bknoles bknoles merged commit fac9eba into inertiajs:master Oct 21, 2024
9 checks passed
@bknoles
Copy link
Collaborator

bknoles commented Oct 21, 2024

Made a couple tweak commits and finally got this thing merged! Thanks so much @ElMassimo ! Lovely code that adds some really nice features.

Thanks also to @skryukov for your sharp eyes to catch a few breaking changes that weren't covered by our test suite. I'm going to follow up with a few specs I created locally to cover those things.

I'll get a release out sometime this week in advance of any Inertia v2.0 work.

@bknoles
Copy link
Collaborator

bknoles commented Oct 27, 2024

Released in 3.3.0!

@bknoles bknoles mentioned this pull request Nov 4, 2024
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants