-
Notifications
You must be signed in to change notification settings - Fork 780
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
Lazy components #606
Lazy components #606
Conversation
Codecov Report
@@ Coverage Diff @@
## master #606 +/- ##
=====================================
Coverage 100% 100%
=====================================
Files 1 1
Lines 139 140 +1
Branches 42 42
=====================================
+ Hits 139 140 +1
Continue to review full report at Codecov.
|
I like the idea, but wouldn't this have a negative impact on testing your app? You wouldn't be able to assert on any VDOM subtree that used a component since it would just be the function and props instead of expanding into the resulting VDOM tree? |
cc75afb
to
3c7b088
Compare
For tests we can use something similar to react's test-renderer import TestRenderer from 'hyperapp-test-renderer'
const testRenderer = TestRenderer.create(<Component />)
expect(testRenderer.toTree()).toBe(theWholeExpectedTreeHere) |
test/h.test.js
Outdated
@@ -67,47 +67,12 @@ test("vnode with attributes", () => { | |||
}) | |||
}) | |||
|
|||
test("skip null and Boolean children", () => { |
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.
@frenzzy Why did you remove this test? Please add it back.
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 test was moved from h.test.js
to components.test.js
because the check for null
, true
and false
was moved from h
to app
runtime.
src/index.js
Outdated
return "" | ||
} | ||
if (typeof node.nodeName === "function") { | ||
return getVNode(node.nodeName(node.attributes, node.children)) |
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.
or maybe
return getVNode(node.nodeName(node.attributes, node.children, globalState, wiredActions))
to make component signature like this:
const Component = (attributes, children, state, actions) => <div {...props}>{children}</div>
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.
(attributes, children, state, actions) => ...
was my first inclination on the component signature -- it's actually the signature that hyperapp-connect used. This signature also makes it a little bit easier for userland HOA/HOC solution development. That signature would obviate the need for #604.
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.
A crazy idea: what if we will create an internal context object:
export function app(state, actions, view, container) {
// ...
var context = { state: globalState, actions: wiredActions }
// ...
}
and will provide it to all components as this
return getVNode(node.nodeName.call(context, node.attributes, node.children))
so, component signature will stay the same but for higher-order components we provide access to internal context which we can use for implementing connect
or Provider
/Consumer
patterns..
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.
Nope. 😄
Interesting idea, I see on this feature an entry point to allow lifecycle event on Components too. Do we know the performance impact ? |
Before I worry about the perf impact, I worry about how this changes the way we build apps, etc. I need to think about this a lot. 😅 |
This looks pretty neat. Can someone post some pseudo-code of the new possibilities this creates? |
Is it me or would this PR allows to implement @zaceno 's hyperapp-context right into core for no performance penalty? |
There's an inescapable (but small, I think) performance hit from maintaining the context boundaries.
No, sorry -- it does not help. Proper context implementation requires resolving the vtree before it's passed to the |
The way this is implemented conflicts with a relatively recent feature, the "ability" to call actions within the view function (/cc @lukejacksonn). A good example of this can be seen in the router here where replaceState is called, which then calls an action thanks to this other code here. The way this is implemented means that an action can be called before patch returns, which will definitely have unwanted side effects, as well as break the lifecycle callback stack. A solution is going back to not allowing calling actions inside the view function. |
Aha! Good point. That's definitely a good argument to process lazy nodes before patch, despite the penalty. (Actions in views is a much more important feature IMO). If only there were a way to have both... 🤔 |
@zaceno I think that's one of the basic points of React Fiber. They solved the problem. |
@jorgebucaran interesting. I haven't looked into React Fiber yet (never looked at React's code at all actually). What was the trick that let them solve the problem (if you know) ? |
@jorgebucaran Thanks for the link! It gave me an idea: I wonder if we could get around the conflict between lazy components and calling actions in views, if we separate the process like how Even if that works... I realize it's quite a long way off. So I think yeah, for now, lets call off lazy components. Actions in views i way more important I think. |
Why do you need to call actions inside a view? Can you provide an example? I can't imagine an example when you need to use actions not from handlers or life cycle events. Also locking action calls during patching is just a one boolean variable. |
I am not a big fan of the idea either, but that's how we implemented the router. I would be happy to remove this feature and think of other ways to move forward. See my comment here. |
@frenzzy Also fetching data on route, like here: https://github.com/lukejacksonn/hyperapp-fetch Correct me if I'm wrong, @jorgebucaran, but this conflict extends beyond just calling actions directly in the view, but also to calling actions in lifecycle events, right? There may be actions you wish to call when a component is created or updated (I can't think of an example, but there are...). Calling the action immediately in the component/view rather than the lifecycle method means the app doesn't have render the whole app once with instantly old state, and then immediately render again. That's also a use case |
No, why? 🤔 We use the lifecycle callback stack to call all the events after patch returns. |
That's a good thing 😄 . I know this debate was had in the past, and I still 100% believe that keeping the view function "pure" without side effects has great benefits. It's part of the reason I don't use the new router. All of these PR's lately are a bit hard to follow. Great to see so much enthusiasm! |
@etalisoft Nice codepen! Cool use of the After some thinking, I believe you're right about multiple apps. If there is a However it's important that you only use a That's a line of thinking I haven't been down, because I was always thinking in terms of having a single mechanism for setting/adding to the context, but only make it apply to the descendants of where the context is set. (That's how hyperapp-context works) Simply having multiple providers gets around that problem. At the cost of having to export a pair of Provider/Consumer components for every branch of context-specialization you need. |
@etalisoft et al. What I wrote above was rambling and wierd. I apologize. I'd delete it but for the record I'm leavign it. Here's a fork of @etalisoft's codepen, which illustrates the basic problem with the Provider/Consumer pattern. https://codepen.io/zaceno/pen/vdzwJr?editors=0010 I'm using the exact same implementation for creating Provider/Consumer components -- the only difference is how I compose them in the views. Notice how it makes no difference wether the provider wraps the component tree or not. It doesn't matter if it's called higher up in the tree than the consumer even. As long as it's called before the consumer in top-down depth-first order, it still works. |
@zaceno What's your point? We want lazy components, don't we? I am really excited about all the new room for activities once this is merged. |
@etalisoft if you will use createContext pattern as it is in your example then it will introduce the most difficult to catch bugs that you have ever seen because of sharing memory. Imagine node.js app, where each user request processed independently and for each request In reactjs they additionally use internal context object which is unique for each app instance to write/read data produced by |
@jorgebucaran Well, as far as I understand the idea of a Provider, is that only Consumers that are descendants of the provider, should have access to what it provides. That is not the case in this implementation (or any implementation I've seen yet -- except hyperapp-context). |
@jorgebucaran Yes that's a version of it. Specifically I was referring to the implementation of Provider/Consumer in @etalisoft 's codepen above. If you look at the fork I made (also linked above) you'll see that it makes no difference where the provider is called, as long as it's before the Consumer in lexical order. I'm quite sure that's not how you want a Provider / Consumer pair to work. And will certainly lead to bugs if you start mixing multiple providers. |
3c7b088
to
bb6dfa2
Compare
@SkaterDad @jorgebucaran I have created a small snippet for the router where there is no action call in the view when fetching data in response of a URL change: https://github.com/hyperstart/hyperapp-recipes/blob/master/router/index.js Is this the kind of implementation you are using, SkaterDad? |
Not sure... I'm not opposed. I've verified that hyperapp-context is still possible with lazy components, but at an additional small performance penalty. Yet I don't really see how it benefits me either. |
@zaceno Possible without lazy components, but you need a |
@jorgebucaran ? - I think you misunderstood me. I meant that https://github.com/zaceno/hyperapp-context can be updated to keep working if we merge lazy components. I was worried it would be permanently broken with no way of implementing context properly on top of hyperapp. I have now verified that is not the case. I'm not opposed to merging this. I'm just not sure I see the gains.
Correct. It is possible to make components lazy "in userland" by using a render prop. I think that would be preferable, since it gives userland control over what we pass into the lazy components -- so hyperapp doesn't have to have an opinon. And it makes laziness opt-in. ... so ok. A teensy bit opposed. |
@zaceno If we don't make components lazy by default, then it's impossible to share the global state and actions with components. That's the other part of this feature. But yeah, I am glad we understand each other now. |
@jorgebucaran @etalisoft demonstrated above how it is possible to share global state and actions with all components -- without lazy components --, using a render prop like you just mentioned |
@zaceno Alright, but I mean by Hyperapp, default and out of the box. const Subview = state => props => <h1>{state.title}</h1>
app(state, actions, () => <Subview />, ...) or const Subview = props => <h1>{props.title}</h1>
app(
state,
actions,
() => (
<div>
{state => <Subview title={state.title} />}
</div>
, ...) |
@zaceno Sorry, I could've avoided this confusion by saying that I meant so from the beginning. 😅 |
@jorgebucaran Ah I see. Ok well my personal opinion on that (I know it's contentious) is:
I also know I've waffled a bit on what my opinions actually are. So... sorry. For now, this is where I stand ;) |
@zaceno I think you can have all the signatures without interference from this feature. I'll give you a proper answer when I get a chance. |
@frenzzy @zaceno Okay, this is what I have in mind. const Subview = props => state => <h1>{state}</h1>
const view = () => {
return (
<div>
<Subview />
<h1>Title</h1>
</div>
)
} This signature: (props => (state, actions) => VNode is backward compatible and lets you do this too: const Subview = props => <h1>{props.title}</h1>
const view = () => {
return (
<div>
{state => <Subview title={state} />}
<h1>Title</h1>
</div>
)
} |
I'm 100% liking this API for components. I don't see any reason to have React-style |
@frenzzy Does it make sense to support vnodes as a function for the root? It seems pointless since you already have the state and actions provided by the view. What's the point of this? const App = props => (state,actions) => <...>
const view = (state, actions) => <App /> I think this is more logical. const App = props => (state,actions) => <...>
const view = App(...) |
const view = () => <Layout>...</Layout>
const view = () => <Router>...</Router>
const view = () => <Provider>...</Provider>
// etc. |
Thanks, @frenzzy! It's merged. I'll be making some changes (components will not be lazy by default) and push up in a few hours. 🎉 |
connect
proposal, ref Add connected components #604h
function with components support, ref Compile JSX right into virtual node objects avoidingh
function #564