-
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
Add connected components #604
Conversation
Codecov Report
@@ Coverage Diff @@
## master #604 +/- ##
=====================================
Coverage 100% 100%
=====================================
Files 1 1
Lines 139 143 +4
Branches 42 43 +1
=====================================
+ Hits 139 143 +4
Continue to review full report at Codecov.
|
Very interesting idea! Will take a closer look at this later. While the framework size increases, I wonder if this would lower the actual application bundles in some cases. EDIT: My big concern with this approach is the performance hit. We're already traversing the entire view before this new |
My question at this stage is more pragmatic. What is the benefits of Connected Components regarding of the explicit use of state and actions properties ? const Component = (props, children) => (
<div>Hello, {props.state.name}</div>
)
const view = (state, actions) => (
<div>
<Component state={state} actions={actions} />
</div>
) Yes, you will need to add |
@jason-whitted I would prefer if people created userland solutions for this like you did with hyperapp-connect. |
From the moment of instantiation an app binds these pieces together. The view is composed of a hierarchy of components. Every instance of each component in the app is directly tied to the state and actions; however, currently they do not have native access to them. Due to this limitation it forces the parent component to be concerned with the business of the child component. This causes tight coupling and a violation of separation of concerns. To bypass this, component developers will be forced to relay action and state down through every component they create to every child.
I would love to do handle this in the 1st iteration, but my unfamiliarity with the codebase is slowing down progress. Could you point me in the right direction? It appears to me that the view does one of two things:
I would assume the
My "userland solution" is a hack at best. You even had questions about why the key was needed. Requiring a HOA and an HOC and the cognitive load required to do a common task goes against one of the main points of Hyperapp.
|
I have updated the code to no longer prematurely recursively inflate before the patch process. It now happens during the patch process. |
Why the name |
A node has children. Some of the children may be functions instead of other nodes. The inflate function is converting them. Hydrate was already used. Inflate sounded good. Haha, have a better name? |
Not sure, maybe |
Or |
I have nothing against the name. It reminded me of this from one of my favorite childhood games: @etalisoft Can you try implementing this touching only |
I renamed |
I thought long and hard on trying to do that. It was the first logical place I tried. I touched on the difficulties previously:
I'm not sure how I could accomplish this, since the |
src/index.js
Outdated
@@ -90,7 +98,7 @@ export function app(state, actions, view, container) { | |||
} | |||
|
|||
function get(path, source) { | |||
for (var i = 0; i < path.length; i++) { | |||
for (var i in path) { |
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.
Why are you touching this? @etalisoft
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.
@jorgebucaran
Given the focus on the end product file size, I was looking for ways to make up for some of the bytes that I added. Obviously this can get reverted as it has no impact on the Connected Component code.
var arr1 = ['a'];
arr1[100] = 'b';
for (var i = 0; i < arr1.length; i++)
console.log('arr1', i);
// outputs 100 times
var arr2 = ['a'];
arr2[100] = 'b';
for (var i in arr2)
console.log('arr2', i);
// outputs twice
Based on the above logic, in addition to savings file size, it seemed it could have the added benefit of possibly reducing iterations.
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.
Let's implement it first, then worry about file size. Please revert it.
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.
arr2[100] = 'b';
Haha yeah, but that can't happen and regular for loops are faster than slow foreach loops.
@etalisoft It's a tough one. What else can we do to mitigate the performance hit? |
@jorgebucaran Is there some kind of performance load test that the team has been using? Maybe something that renders thousands of children and fires state changes. If not, maybe it's the next thing I build :) I'd like to compare the before and after performance. I think the performance impact will be extremely minimal. |
@etalisoft You are iterating over children twice every time. |
Not any more! 😉 |
@etalisoft You forgot to revert the for..in loops to for loops. |
Since context was brought up again (not my fault this time! 😅 ): If Redux's connect function is based on React's context, wouldn't it make sense to have context in Hyperapp, and solve the connected-components goal through that? PS
|
@zaceno @jorgebucaran I created a modified version of const Component = (props, children, context) => <div>{children}</div>; The context is just a plain object. It is cloned from the parent object. If a component wants to add something to the context for it's children they just do it in the function body. const Parent = (props, children, context) => {
context.apiKey = "abc123";
return <div>{children}</div>;
}
const NestedChild = (props, children, context) => <div>{context.apiKey}</div>; With this ability I then created a If you want to take a look, I put it on CodePen. This is obviously a completely different solution. // `current` solution
const Component = (props, children, { getState, getActions }) => ...
// `context` solution
const Component = (props, children, context) => ... The The |
If in order for this to exist in userland without severely patching the app function we need to introduce a feature like React's context, then let's look into it. We can't simply have all the features, but we can add something new that opens the door to new, exciting and generalized possibilities. |
Nice solution for context! And you're right -- there is a penalty to it. Not sure if you're deep-cloning or shallow, but I think shallow should be enough and the impact should be negligible. Still: it's there. ..and would annoy anyone who doesn't want/use it. Your connected-components approach is essentially free, and as such "harmless" to those who don't care about it. On the other hand: not as flexible as context. |
I agree that hyperapp must have a way to pass things (global state/actions/context/etc) down to components without the need to do it manually (using props through the whole tree). I also agree with @jorgebucaran that core should not implement all the features but just allow you to extend it in userland. In my mind we have two ways how to achieve this (an ability to communicate parent and child components without modifying components tree in the middle): 1. Context objectFor example via component signature: const Component = (attributes, children, context) => <div />
// or
const Component = (attributes, children) => context => <div /> But I do not see a way how to make it explicit and simple to understand/use for users. Example above with 2. Global actionsFor example with one of the following component signatures: const Component = (attributes, children, actions) => <div />
const Component = (attributes, children) => (actions) => <div />
const Component = (attributes, children, state, actions) => <div />
const Component = (attributes, children) => (state, actions) => <div /> By using global actions you can solve the same problems which context solves. But actions already described by hyperapp and you know how to use them. And you also can implement your own mutable context based on actions or anything else. By the way, React.js team changed context api trying to make it more explicit but they do not have global actions. So, IMO we should approve only something like #604 to be in the core. |
This is not exactly true. Context isn't necessarily about the state, but about where in the component tree a component is being rendered -- not something you can keep in state, or use actions to update. Because the second you use an action... boop! The tree is being rendered again. |
@frenzzy Have you checked out hyperapp-context? Would you agree that such an API would be an elegant solution for 1. that has much more possibilities than 2.? |
@zaceno under the hood context is just an object which is available in all components. You can add any features to it to behave as you want. For example on a component level you can detect where you are and add this into to context. Context implementation example using global action: const context = { key: 'value' }
const state = {}
const actions = { getContext: () => context }
const ChildComponent = () => (state, actions) => {
const context = actions.getContext()
return <div>context: {context.key}</div>
}
const ParentComponent = () => <div><ChildComponent /></div>
const view = (state, actions) => <main><ParentComponent /></main>
app(state, actions, view, document.body) or you may use higher order component: const withContext = (component) => (attributes, children) => (state, actions) =>
component(attributes, children, actions.getContext())
const YourComponent = withContext((attributes, children, context) => <div />) |
@frenzzy As far as I understand, in hypperapp-context's case, any value set on the context is only available to the children of this component, not to the entire tree, as such I don't think you can (easily) implement it with an action/state slice. |
@Mytrill it is solvable with a knowledge about component executing order. For example how implementation of new React.js context API may look in Hyperapp: function createContext(defaultValue) {
let value = defaultValue
return {
Provider(attributes, children) {
value = attributes.value
return children
},
Consumer(attributes, children) {
return attributes.render(value)
}
}
}
const ThemeContext = createContext('light')
const view = (state, actions) =>
<main>
<ThemeContext.Provider value="dark">
<div>
<ThemeContext.Consumer render={val => <div>{val}</div>} />
</div>
</ThemeContext.Provider>
</main>
app({}, {}, view, document.body) Demo: https://codepen.io/frenzzy/pen/oEyGeJ?editors=0010 It does not work right now just because component execution order must be top to bottom, but currently components are executed in the opposite order. Lazy components PR #606 will solve this problem. |
@frenzzy Yes, that implementation looks more workable. It works because it does not use state/actions to read & create context. That's the only point I was making (and I think @Mytrill ). Like you say, it requires "lazy components". Actually, your implementation of context, when you include the lazy-components aspect, is technically pretty much how my |
@jorgebucaran @frenzzy @Mytrill @zaceno I took @frenzzy's // Could be the output of a hyperapp-context package
const createContext = defaultValue => {
let value = defaultValue;
return {
Provider(attributes, children) {
value = attributes;
return <div>{children}</div>;
},
Consumer(attributes, children) {
return attributes.render(value)
},
};
}; Created // Could be the output of a hyperapp-connect package
const { Provider, Consumer } = createContext({});
const connect = (mapStateToProps, mapActionsToProps) => BaseComponent =>
(props, children) => {
const render = ({ state, actions }) =>
<BaseComponent {...props} {...mapStateToProps(state)} {...mapActionsToProps(actions)} />;
return <Consumer render={render} />;
}; The end result is context AND connected components -- and the only thing that needs to be added to the hyperapp core library is #606 (@frenzzy's Lazy Components) and #611 (@titouancreach's fix minify). import { h, app } from 'hyperapp';
import { Provider, connect } from 'hyerpapp-connect'; // uses hyperapp-context internally
const state = { value: 0 };
const actions = {
value: value => state => ({ value }),
};
const Component = (props, children) => <div />;
const mapStateToProps = state => ({ value: state.value });
const mapActionsToProps = actions => ({ setValue: actions.value });
const ConnectedComponent = connect(mapStateToProps, mapActionsToProps)(Component);
// The ConnectedComponent would receive the following props:
// name: string
// value: number
// setValue: function
const view = (state, actions) => (
<Provider state={state} actions={actions}>
<ConnectedComponent name="bob" />
</Provider>
); Here's a working CodePen. |
Meaning this PR is no longer relevant? Or did I misunderstand what you said? And why #611? What difference did it make? |
Correct, if lazy
The minified code was not properly relaying props down to child nodes. The unminified code did not have this problem. When I incorporated #611 the minified code worked properly. |
I know someone who loves lazy comments. |
comments === components? |
Ha 😃 That’s what I get for typing on my phone. Additionally, lazy components makes #615 irrelevant also |
I think we are fast approaching a major decision point for this project. Right now we have a pretty equal compromise between the simple/functional/verbose and complex/imperative/performance/pragmatic sides. If we choose to support connected components - that would be a shift toward the complex side in order to make certain things easier and more like React with somewhat stateful components. My vote is to keep things simple in core, unless we can come up with another approach that is simple made easy 👌 |
That approach is more flexible and allows for the community to decide how to wire their components. Maybe some awesome approach (à la selectors) will emerge. Probably it's something no one has even thought of yet. |
@okwolf Selectors, like reselect? If so, I’ve already codepenned that in one of these threads. They already work and pair perfectly with Provider/connect model listed earlier. |
Conceptually, yes. I'd love to see an approach that improves on Reselect by integrating more tightly with the VDOM engine. This would give some of the same ergonomic improvements as the shorthand syntax for actions has over reducers. |
Wow, just in time 👍 |
@etalisoft I think we can close here then? |
@jorgebucaran Yes, #606 is a more flexible solution than this PR. This PR can be closed. |
Closed in favor of #606. |
It's easier to document this way. Thanks to @etalisoft for documentation bits from #604.
Connected Components
Why?
Currently all components are dumb components. Given a component hierarchy A->B->C->D->E. If Component
E
needed access tostate.name
then components A, B, C, and D would each need to be updated to pass along state.name. This causes components to become tightly coupled, or written generically so that all state and actions are handed down to every child component, because of the "you never know when you'll need it" mentality.The Solution
Natively support both dumb components and connected components.
Normal dumb component signature:
Connected component signature:
This solution does not require use of either a Higher Order App or a Higher Order Component.
How does it work?
In hyperapp's
render
function, whenever the view is processed, the resulting object tree is traversed. Any child node that is a function is called with theglobalState
andwiredActions
.This change raised the gzip size by ~30 bytes.