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

Alternative to helper mixin "anti-pattern" #310

Closed
jaredcwhite opened this issue Apr 16, 2020 · 11 comments
Closed

Alternative to helper mixin "anti-pattern" #310

jaredcwhite opened this issue Apr 16, 2020 · 11 comments
Labels

Comments

@jaredcwhite
Copy link
Contributor

Feature request

One of the things I first set up in using View Component on a project was to bring in common helpers I'd been using in traditional view templates. So for example I have this line in my component superclass to get the logged-in user record as well as the relevant Pundit policy:

delegate :current_user, :policy, to: :helpers

But I'm kind of feeling like that's an anti-pattern at this point, and I'd prefer some other mechanism where I define a data flow from the top level (a controller probably, but maybe another component) and have that get passed along down the component chain automatically. I suppose that's somewhat similar to React's Context API which I'm a bit familiar with. And then the component's template would just need to be smart to check for the presence of that inherited data and not crash if it's missing.

Motivation

I was never religiously anti-helper but I'm somewhat in favor of doing away with them entirely in the new View Component era and look for a new pattern for "global" data/tooling.

If anyone think this would be good to investigate, I could try to prototype an approach or two for the API and see what the consensus is. Or if you already have something like this in the works for View Component, I'm all ears. 😃

@jaredcwhite
Copy link
Contributor Author

Sorry to comment on my own issue 🤣, but I just realized because the component's view template can access all the accessors from the component .rb, I think it becomes even more important to roll with the assumption that any local variables/methods in the template come solely from the component and aren't a bunch of helpers from afar (other than perhaps common Rails conventions like forms, links, etc.).

@joelhawksley
Copy link
Member

@jaredcwhite we just worked on a first attempt at providing current_user to our components.

In our ApplicationComponent, we have:

def current_user
  helpers.current_user
end

And then in our app-specific component test helpers, we have:

def as(user)
  controller.send(:current_user=, user)
end

Which allows us to write tests like:

as(@user) 
render_inline(MyComponent.new)

assert_text @user.name

However, we're not sure if this is the best approach- it's just our first attempt.

I'm generally wary of making global state too easy to use, as it kind of defeats some of the advantages of components in the first place. For one, it makes testing trickier, as you now have to set that global state indirectly to test the component behavior, as with the as helper above.

@jonspalmer
Copy link
Collaborator

@joelhawksley Curious why you aren't passing current_user as a parameter to the component?

Why isn't it

render_inline(MyComponent.new(user: @user)

assert_text @user.name

@jaredcwhite I'm kind of anti helper too. ViewComponents using url of form helpers makes sense - they're essentially static methods. However, the helpers that rely on or derive state (like current_user) should be passed explicitly. It's analogous to the difference between explicitly passing locals to a partial vs declaring instance variables.

@joelhawksley
Copy link
Member

@jonspalmer we had some cases where components were passing current_user down three or four levels of nesting. It wasn't pretty. That being said, there's a reason that code ☝️ isn't in the README- we're not sure if it's a good idea or not.

Generally, we too are hoping to avoid the use of stateful helpers.

@jaredcwhite
Copy link
Contributor Author

I think I'll pursue some kind of passing a simple "context" object down the chain…at least then the method signatures don't need to be updated everywhere to go from current_user to something else or support multiple state values. At some point further down the road I'll report back and see if I liked it. 😄

@jonspalmer
Copy link
Collaborator

@joelhawksley makes sense. I haven't hit that problem yet. I suspect we should be mindful of the advice the React folks give on other ways to avoid needing this kind of context: https://reactjs.org/docs/context.html#before-you-use-context

I would imagine that a Context API inspired by React Context would make sense. I don't immediately see how we'd implement it. Could we implement some special wrapping ViewComponentContext component that you surround other things with?

@tomasc
Copy link
Contributor

tomasc commented Jun 14, 2020

@jaredcwhite how about the CurrentAttributes api? I have been trying to avoid this so far, but wonder whether this would not make things easier (I need to pass around quite some information about current state).

Perhaps better solution than helpers, which mix (in @joelhawksley’s example) both static methods and current state. I would prefer to save Rails helpers for static methods (such as tag and url helpers) only. Using the CurrentAttributes api would at least differentiate between the two and I guess would do similar job as the React-inspired context @jonspalmer suggests.

?

@jaredcwhite
Copy link
Contributor Author

jaredcwhite commented Jun 28, 2020

@tomasc Good suggestion! In fact, I ended up going with that approach. Since I ended up adding in a little trick regarding the Pundit permissions, I'll include code samples from my app.

Here's my Current class:

class Current < ActiveSupport::CurrentAttributes
  attribute :user, :policy_method
end

Then in my ApplicationController, I added a before_action callback:

def initialize_component_context
  Current.user = current_user
  Current.policy_method = method(:policy)
end

The reason I'm saving a policy method as a proc, rather than running the method right here, is because policy accepts an argument with the object for which you need a policy, so that needs to be run later with the object in question. So I use that in my component like so:

def policy_update
  Current.policy_method[@comment_record].update?
end

The other cool part about this is I can set up a policy as part of a component preview. In fact, that's what triggered this solution in the first place—my Pundit policy helper was missing in the controller preview context. So here's the preview:

class CommentComponentPreview < ViewComponent::Preview
  def example_signed_in
    comment = previewable_comment
    render Bank::CommentComponent.new(
      bank: comment.commentable,
      comment: comment
    )
  end

  def example_signed_out
    comment = previewable_comment(author_policy: false)
    render Bank::CommentComponent.new(
      bank: comment.commentable,
      comment: comment
    )
  end

  private

  def previewable_comment(author_policy: true)
    Comment.where(commentable_type: "Bank").first.tap do |comment|
      Current.user = author_policy ? comment.user : nil
      Current.policy_method = proc do
        CommentPolicy.new(Current.user, comment)
      end
    end
  end
end

Notice that last part where I create a proc inline to instantiate a Pundit policy for the comment, and I can control which user I load the policy for in order to preview both signed in and signed out examples.

If you have any further feedback or suggestions, I'm all ears!

@tomasc
Copy link
Contributor

tomasc commented Jun 29, 2020

Hi @jaredcwhite thanks for the examples. I don't use Pundit (I use CanCanCan & ActionPolicy), but it seems clever what you are doing. Thanks for sharing!

@jrochkind
Copy link

I'm just getting started understanding view_component.

But the general problem of "passing [eg] current_user down three or four levels of nesting" is something the React community has run into too; makes sense, since similar pattern. I am not super experienced with React either, but looking around they sometimes refer to this problematic pattern as "prop drilling" (https://kentcdodds.com/blog/prop-drilling), and recent React has some kind of "context" I don't fully understand to try to deal with this: https://reactjs.org/docs/context.html

It might be useful to understand what they're doing there, to learn from that in trying to do something similar in Rails, perhaps using the Rails CurrentAttributes.

@stale
Copy link

stale bot commented Aug 29, 2020

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the stale label Aug 29, 2020
@stale stale bot closed this as completed Sep 5, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants