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
Comments
|
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.). |
|
@jaredcwhite we just worked on a first attempt at providing In our ApplicationComponent, we have: def current_user
helpers.current_user
endAnd then in our app-specific component test helpers, we have: def as(user)
controller.send(:current_user=, user)
endWhich allows us to write tests like: as(@user)
render_inline(MyComponent.new)
assert_text @user.nameHowever, 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 |
|
@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. |
|
@jonspalmer we had some cases where components were passing Generally, we too are hoping to avoid the use of stateful helpers. |
|
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. 😄 |
|
@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? |
|
@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. ? |
|
@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
endThen in my def initialize_component_context
Current.user = current_user
Current.policy_method = method(:policy)
endThe reason I'm saving a policy method as a proc, rather than running the method right here, is because def policy_update
Current.policy_method[@comment_record].update?
endThe 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
endNotice 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! |
|
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! |
|
I'm just getting started understanding 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 |
|
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. |
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:
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. 😃
The text was updated successfully, but these errors were encountered: