[RFC] Components & Mixin Guidelines
See original GitHub issueMixins and components are not the same thing!
We use components to describe parts of our app, like a TabBar, a ToggleButton, SlideButton, etc., and mixins to extend the state, actions and events of our app.
To extend means that we will merge your mixins state/actions/events with your app state/actions/events to create a single state/actions/events.
But often you need to pass actions to your component via props.
function ToggleButton({ toggle, isOn }) {
return ( < /* ... */ > )
}
app({
actions: {
toggle(state) {
return { isOn: !state.isOn }
}
},
view(state, actions) {
return <ToggleButton toggle={actions.toggle} isOn={state.isOn} />
}
})
This makes components, in a way, really “hollow” as we must teach it pretty much everything so it can thrive in the world. This is good, as it makes components extremely easy to test and debug. But what if the implementation of those actions is provided by a mixin?
In that case I think the mixin and component should be tucked away into their own module.
A good example of this is our friendly Router’s Link component.
import { router, Link } from "@hyperapp/router"
app({
view: [
[
"/",
(state, actions) =>
<Link to="/test" go={actions.router.go}>
Test
</Link>
],
[
"/test",
(state, actions) =>
<Link to="/" go={actions.router.go}>
Back
</Link>
]
],
mixins: [router()]
})
Notice how we must teach Link actions.router.go
, which is fed to our actions by the router()
mixin. I mean, it’s not so bad right? Just a little repetition is not so bad and this makes the Link component easy to test and debug and all that, right?
Sure! but the Link component would be just as easy to test and debug and all that if it shipped with actions.router.go
already “pre-wired”… as long as you can overwrite the action and pass it a new one, just like it’s done above.
There is a PR here addressing just this. The idea is to be able to rewrite the example above as follows:
import { h, app } from "hyperapp"
import { router } from "@hyperapp/router"
app({
view: (state, actions, { Route }) => <Route />,
mixins: [
router([
[
"/", (state, actions, { Link }) =>
<Link to="/test">
Test
</Link>
],
[
"/test", (state, actions, { Link }) =>
<Link to="/">
Back
</Link>
]
])
]
})
Notice now we don’t need to teach the Link actions.router.go
anymore! 💯
On the other hand, see that we now need to grab the pre-wired Link component somehow, since:
import { Link } from "@hyperapp/router"
…would take us back to square one (wouldn’t work unless we pass actions.router.go
ourselves).
The benefit is less boilerplate, but it comes at a cost: we need to overload the view
function with a third argument! 🤔
The good news is that none of this requires changes in core! 🎉
This is already possible using events.render
.
But the real question to me is which way is more expensive? Overloading the view function or having to manually wire components every time we use them?
The only caveat about overloading the view function is you need to make sure not to break other authors also overloading the function. This is already solved, of course, using “namespaces”, which fortunately we already support.
actions: {
myNamespace: {
// My actions
},
yourNamespace: {
// Your actions
},
...
}
Now, while the Router and Link component are good examples to introduce this idea, we could easily shrug it off and get away without it just as we have done until now.
IMO more complex components will benefit from this pattern. Imagine a rich text editor in Hyperapp.
It could look as follows:
import { h, app } from "hyperapp"
import { editor } from "./editor"
app({
view(state, actions, { editor: Editor }) {
return <Editor />
},
mixins: [editor({/*...*/})]
})
Compare how it might look like without overloading the view function:
import { h, app } from "hyperapp"
import { Editor, editor } from "hyperapp-rt-editor"
app({
view: (state, actions) => (
<Editor state={state.editor} actions={actions.editor} />
),
mixins: [editor({/* ... */})]
})
Not the worst, but I prefer the previous pattern. What about you?
Whichever style you end up choosing for your own components+mixins is ultimately up to you.
👋
Issue Analytics
- State:
- Created 6 years ago
- Comments:16 (13 by maintainers)
Much prefer
<Editor />
over<Editor state={state.editor} actions={actions.editor} />
👍 for
view(state, actions, {editor: Editor}) {...}