-
Notifications
You must be signed in to change notification settings - Fork 25
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
Differed init + update + lifecycle 'hooks' #71
Differed init + update + lifecycle 'hooks' #71
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really like how clean this is.
class << self | ||
alias_method :_new, :new | ||
def new(*args) | ||
Wrapper.new(*args) { |*arguments| _new(*arguments) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the idea of passing the block here, so Wrapper
doesn't need to assume it's calling klass._new(*args)
. We may be able to skip taking *arguments
in the block, though. They should be the same as *args
if I understand the flow here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it should be able to just use *args
, but I had issues when I didn't explicitly define *arguments
. Opal on occasion does some mysterious things with passing blocks, and when I see something weird happening, I default to just being verbose.
else | ||
props | ||
end | ||
def before_unmount |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we go with the Thunk
approach, we may only be able to provide unmount hooks with a virtual-dom
unhook
, but I'm not 100% sure if that'll work. I'm reasonably sure that, unless you provide the same instance every time, unhook
will be called on every render.
The good news is that unhook
takes a next
value, which will probably be null
or undefined
when the component is actually unmounted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, good catch. I completely forgot about before_unmount
. I'll tack on an unhook
and see what happens.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Crap, it looks like hooks don't work on thunks — only vnodes. I wrote a quick UnmountObserver
class for Wrapper
to use (specifically, Wrapper
set one up as an instance variable for virtual-dom
to read as one of the properties of the thunk) and hooks weren't firing. When I replaced …
return #{component.render}`
… with …
return h('div', { observer: #@unmount_observer }, [#{component.render}]);
… they began firing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting. Well, that's unfortunate, but honestly, I personally could survive without before_unmount
. The extra hooks are nice to have, but for me, these StickyComponent
PRs are about components keeping state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a great point. It's a good idea to stick with the simplest thing that works, which means foregoing callbacks until a need presents itself. If the only way to do certain things is via callbacks, we can revisit it then.
I guess I just got too focused on making them work if we're going to provide them at all. :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, I dropped the callbacks; should probably figure out how to properly test this.
props = nil | ||
class << self | ||
alias_method :_new, :new | ||
def new(*args) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One of the reasons I used .render
instead of .new
was so you could choose use a StickyComponent
instance as a regular component. I don't know if that's a reasonable thing to do, though. What are your thoughts on that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I land on the side that if you define a StickyComponent
, then you have a StickyComponent
. If you need a component to behave both sticky and non-sticky, just make a sticky wrapper component for a non-sticky component. Well defined responsibilities for each type of component will encourage smaller composable classes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well said.
It looks pretty good so I went ahead and merged it into my PR. I'm still a little iffy on overriding |
Yeah, overriding Okay, this might go against the grain of my earlier comment, "if you define a StickyComponent, then you have a StickyComponent", but what if the sticky facility became less about the specific 'type' of component and more about telling any component how you want it to behave in a given context. Add this to the component module: module Clearwater
module Component
def self.sticky(*args)
StickyWrapper.new(*args) { |*arguments| new(*arguments) }
end
end And just have a class StickyWrapper
attr_reader :component
def initialize(*args, &block)
@args = args
@block = block
end
%x{
Opal.defn(self, 'type', 'Thunk');
Opal.defn(self, 'render', function Component$render(previous) {
var self = this;
if(previous &&
previous.vnode &&
this.klass === previous.klass &&
previous.component) {
self.component = previous.component;
if(#{!component.respond_to?(:should_update?) || component.should_update?(*@args)}) {
#{component.update(*@args) if component.respond_to?(:update)};
return #{component.render};
}
return previous.vnode;
} else {
self.component = #{@block.call(*@args)};
return #{component.render};
}
});
}
end
end When invoking What do you know, another option on how to implement this. |
* Differred init + update + lifecycle 'hooks' * make initialize call update by default * Remove callbacks per Dr. Ian Malcolm
* Sticky components experiment * Call will_mount before render * Differed init + update + lifecycle 'hooks' (#71) * Differred init + update + lifecycle 'hooks' * make initialize call update by default * Remove callbacks per Dr. Ian Malcolm * Swap back to the render class method This commit also adds a warning if you use .new instead of .render * Make sure to always render when using instances * Add sticky wrapper * Switch to MemoizedComponent * Fix spec by passing a real Renderable * Remove test knowledge of BlackBoxNode::Renderable * Remove a bunch of WIP experiments
I spent some time thinking about the approaches and discussion in #70 and came up with this. It still suffers from some tradeoffs (possibly two places dealing with passed in args), but for me feels a bit closer to the "right" abstraction.
In this implementation of
StickyComponent
:initialize
is called only once per instance with the arguments given tonew
.update
method that receives args for subsequent renders. This is the formal place to operate on passed in properties.initialize
delegates toupdate
to avoid duplication, but can easily be overridden.will_
anddid_
methods are replaced with 'hooks'. I don't see any reason for these to be passed arguments since they'll be the same values until the next render. I do, however, find it useful to be able to hook into various stages of the lifecycle. For example,before_update
would be to save current state before it is replaced instead of adding additional logic to theupdate
method.