-
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
Sticky components experiment #70
Conversation
Also, the 2x performance difference I saw with the |
I definitely think Clearwater should support stateful components. I agree in most cases it is better to manage state outside of the component, but there are cases where it is needed. A re-usable, toggle component, that is ignorant of the Store or state structure is a perfect example. Every time I need a component to behave like this in Clearwater, it is like hitting a wall. It has forced me to roll my own |
Yeah, I've ended up throwing stuff that would ideally be component state into the data store to get around it, which is kinda gross. In addition to the obvious app-state bloat that comes with this approach, the rest of my app shouldn't have access to something that's specific to a single component. I've even used It's hard to find the "right" abstraction here — using quotes here because none of them are really right. They all involve tradeoffs that are terrible for some use cases. I also don't particularly like having to use something like I'm a little sensitive to how this works because I have a feeling people will reach for this a lot more than I would — Ruby developers are known for preferring convenience over all else and not having to write constructors and |
I have done that as well. It worked, and since I only had to do it in a couple isolated cases, I didn't do any performance testing. Is there a reason for treating class StickyComponent
def self.new(*args)
instance = super
instance.instance_variable_set("@props", args)
instance
end
end This would remove the need for the |
Same. I've only done it three times, and finally got fed up and decided to come up with this PR. :-)
I really wanted to do something besides And then, if we give people control over their args, it means that
But then |
The only way around all that that I could think of would be to call class DropdownMenu
def initialize(menu_options)
@options = menu_options
@open = false unless defined? @open # Weird example, can't use ||= to memoize booleans
end
end I'm not sure this is the worst idea, tbh. Admittedly, I originally abandoned it without much thought for lifecycle reasons alone, but in the larger picture, this feels cleaner than props. I understand why React uses props+state after this whole exercise, but I don't like it any more than I did before. :-) |
@ajjahn I went back to using the Also, tried out the div([
sticky(Counter, 12) { do_the_thing },
Counter.new(12) { also_do_the_thing },
]) |
I think it is reasonable to err on the side of the principle of least surprise and avoid overriding Also, I think it is possible I'm over looking something, but I'm not sure I understand the separate very similar, but separate def sticky(klass, *args, &block)
Wrapper.new(args, block) { klass.new(*args, &block) }
end That would get pretty close to #71 (comment). Do you have any thoughts or feedback on that variation? |
It's very similar. In fact, I copy/pasted the code in at first, but the main difference is that the block in this one is intended to be passed to the component constructor. The reason we take the class is so that we know what the class is of the new one for invalidation purposes. For example: div([
logged_in? ?
sticky(SessionInfo, current_user) :
sticky(NewSession),
]) vs div([
logged_in? ?
sticky { |current_user| SessionInfo.new(current_user) } :
sticky { NewSession.new },
]) In the second example, we would end up trying to tell a However, now that I mention that, if they were reordered without setting a
This is excellent feedback. Do you have any other ideas?
It's what inspired this experiment. I really like it. Composition over inheritance and all that jazz. I'm just playing around with different ways of doing things to see what feels good to use that also works well in some of the weird edge cases that are sure to crop up. :-) |
Ah, yes, in fact leaving the class argument out of my example on #71 was an accidental omission. It is supposed to be there. As far as naming the factory method, when reading a typical component's render method, you'll encounter something the resembles Communicating about code is hard, so please forgive me if you feel you've addressed this and I'm just missing it, but what I'm still wondering is why have a Here's what I'm getting at, using 'Persist[ent]' instead of 'Sticky': module Clearwater
module Persistent
def self.included(klass)
klass.extend(ClassMethods)
end
module ClassMethods
def persist(*args, &block)
Wrapper.new(self, *args, &block)
end
end
class Wrapper
attr_reader :component
def initialize(klass, *args, &block)
@klass = klass
@args = args
@block = block
end
%x{
Opal.defn(self, 'type', 'Thunk');
Opal.defn(self, 'render', function Persistent$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)}) {
if(#{component.respond_to?(:update)}) {
#{component.update(*@args)};
}
return #{component.render};
}
return previous.vnode;
} else {
self.component = #{@klass.new(*@args, &@block)};
return #{component.render};
}
});
}
end
end
end module Clearwater
module Component
def persist(klass, *args, &block)
Persistent::Wrapper.new(klass, *args, &block)
end
end
end Make a component persistent: class MyComponent
include Clearwater::Component
include Clearwater::Persistent # Optionally, this could easily be included in all components by default.
#...
end Using a persistent component: div({}, [
MyComponent.persist(my, args) { and_block },
persist(SomeComponent, and_some, args) { and_block },
]) Maybe I should have just PR'd that instead of writing all the code in a comment. |
I agree, "sticky" is not my favorite name for this. I'm not sure about "persistent", but it's probably an improvement, at least. My main friction with it is that, when I think of "persist" in terms of software, I think of either persistent data structures or object persistence into long-term storage — this doesn't really fit either description very well. So, like
I think the idea of composition here is interesting and I'm experimenting with it, but a I haven't come to any conclusions. It's been rare that I've found a real need for stateful components, so I'm still feeling things out. |
Yeah, "persistent" is overloaded. Others that come to mind: "perpetual", "fixed", "recurrent", and "perennial." Naming is hard.
Not really. It is just as easy to create a sticky component that simply wraps whatever component that wasn't designed for stickiness. So, yeah, I think I'm back to where I started on that one. |
I'm still really glad you brought it up. I wouldn't have even thought about it otherwise. So many different ways to slice this pizza and I think there's value in at least exploring them. |
How do you feel about class MyComponent < Clearwater::MemoizableComponent
end
class Layout
include Clearwater::Component
def render
div([
# ...
MyComponent.memoize,
])
end
end |
I like it! |
* Differred init + update + lifecycle 'hooks' * make initialize call update by default * Remove callbacks per Dr. Ian Malcolm
This commit also adds a warning if you use .new instead of .render
e9b9fb6
to
83b3908
Compare
One of the tradeoffs of current Clearwater components is that they're ephemeral. In a lot of ways this is really great because then you can treat them simply as objects. However, it means that you can't store state on them — they're effectively immutable. Any state has to be stored outside of them and passed down the component tree from some component layer that knows about that store.
This PR includes two experiments. Each of them makes it possible to create stateful, React-style components — I've called it a
StickyComponent
because naming things is hard. Instead ofMyComponent.new(...)
, you writeMyComponent.render(prop1: prop1)
(it takesprops
like React components) to put it into the render tree. For example:In this example, we render a button that toggles between
true
andfalse
. Nothing fancy, but it exhibits how nothing outside of this component needs to know about its "on" state. Note how we did have to get properties that were passed into here fromprops
. It would be nice not to have to do that, but we can't get around that while still guaranteeing thatinitialize
is called only once in the component's lifecycle.Differences between experiments
I mentioned above that I included two experiments. You can find them in the diff as
Wrapper
andWrapper2
because of my incredible skill in naming things.Wrapper
hijacks the diff phase the same wayCachedRender
does. It calls a few other methods to emulate React lifecycle methods primarily to show off that it can, but also because they can be useful if you have a stateful component. It's not much different internally fromCachedRender
except for lifecycle hooks, so performance should be close.Wrapper2
uses aClearwater::BlackBoxNode
to take even more control of the rendering process. It provides the full spectrum of React-style lifecycle hooks.The downside of this approach is that it's quite a bit slower than normal Clearwater rendering. My benchmarks of the second approach show that it can take about 2x as long to update than Clearwater's traditional rendering algorithm. In light of this, I added an
async_render
prop, which provides support for something like I described in #57. This only works forBlackBoxNode#update
, and takes longer overall, but it gets some content in front of users faster.I wanted to start a discussion here to see if we want to support any of this with Clearwater. I admit, stateful components are nice to have sometimes, especially for components provided in a gem. Nobody who uses my gemified component should need to worry about how it stores its internal state. However, I wonder if the rest of it is necessary at all.