NOTE: this article series is superceded by this article
In the last couple parts I've focused a lot on actions, but haven't said anything about how to handle effects or subscriptions. There isn't all that much to say, but for the sake of completeness:
Subscriptions
Every time the state changes, Hyperapp calls the subscriptions
property you provide to app({...})
, and expects it to return an array of all the subscriptions your app needs to respond to.
This is similar to how Hyperapp calls the view every time the state updates, to find out how the DOM should look. We can break out subscriptions in a very similar way to how we broke out views in part 2.
const counterSubs = model => [
onKeyDown('ArrowUp', model.Increment),
onKeyDown('ArrowDown', model.Decrement),
]
//...
app({
//...
subscriptions: state => [
...counterSubs({
Increment: IncrementFoo,
Decrement: DecrementFoo,
}),
// other subs...
]
})
The list of subscriptions doesn't typically grow as fast as the view does – or even actions. There is no strong reason to do anything about it until you start breaking out actions in separate modules (parts 4-5).
If your subscriptions need actions that have been moved in to a module, you should break the subscriptions out as a subscription-component, and move it to the same module. It should receive the actions in the same way view-components in the module do: via a model. That way the model and its contents becomes a secret known only to the module.
import * from './counter.js'
const foo = counter.wire({/* getter, setter, et.c.*/})
app({
//...
subscriptions: state => [
...counter.subs(foo.model(state)),
// other subs...
]
})
Effects
Effects are returned from actions and that doesn't need to change – even if/when you've moved your actions from a monolith to a wire
function.
const wire = ({getter, setter, onData}) => {
const GetNewData = state => [
setter(state, {...getter(state), fetching: true}),
httpGet('https://example.com/data', GotData) // <--
]
// this action doesn't need to be part
// of a model since only GetNewData needs it.
const GotData = (state, data) => onData(
setter(state, {
...getter(state),
fetching: false,
data,
})
)
//...
}
Effects and Mapped Transforms though...
The only trouble, really, is when you want an effect to run with a mapped transform.
Mapped transforms, as you'll recall from part 5, are similar to actions, but aren't dispatched in response to events. They are functions that one module makes available to be called from another module's actions.
Their similarity to actions means that you sometimes want them to be able to return effects.
Say you have a mapped transform for "dealing cards". Some other action which knows more about the rules of the game will call this transform. However, the domain-logic of cards (a secret of cards.js
) say that when the deck is used up, a new deck needs to be shuffled and dealt. It might look like this:
//this is cards.js
//...
const wire = ({getter, setter, ...other}) => {
//...
const _deal = (state) => {
if (!cardsRemaining(getter(state))) {
return [state, shuffle(DECK, DealNewDeck)
} else {
return setter(state, deal(getter(state)))
}
}
return {
deal: _deal
//...
}
}
//...
Whichever action is going to call deal
, it will need to handle that it sometimes returns a state-effect-tuple rather than just a new state. That action then needs to make sure it returns a state-effect-tuple which includes the effect that deal
returned.
That will make the action pretty cumbersome to implement. Besides, the modular separation would be better if this wasn't something other modules needed to think about.
Most often (I think) it is possible to avoid this situation by design. Try that first. Otherwise, just accept that nothing can be perfect, make peace with breaking modular purity and move on.
Meanwhile, on the Dark Side...
Or...? Well, there is a hack (strong emphasis on "hack") that lets you run effects without needing to return them from actions. I'm not recommending it, but it might at least be interesting to know about.
Taking the example above, instead of deal
returning a tuple, you would implementent it as:
const _deal = (state) => setter(state,
cardsRemaining(getter(state))
? deal(getter(state))
: {...getter(state), needsNewDeck: true}
)
You'll also need an action for what to do when we need a new deck:
const GetNewDeck = (state) => [
setter(state, {...getter(state), needsNewDeck: false}),
shuffle(DECK, DealNewDeck)
]
See how we no longer return the effect from the mapped transform? Instead we moved it to a proper action that is meant to be dispatched. But how to dispatch it? – That's where the hack comes in:
You can create a custom subscription function which takes the model as a parameter. That will get the subscription function to run every time the model changes. From there you can dispatch GetNewDeck
:
const mySub = (dispatch, model) => {
requestAnimationFrame(() => {
model.needsNewDeck && dispatch(model.GetNewDeck)
})
return () => {} //noop
}
const subs = model => [
[mySub, model],
//...other, real subscriptions I might need
]
A subscription function that executes every time the state changes is definitely not how subscriptions are meant to be used. The requestAnimationFrame
is there only to work around implementation details of how Hyperapp schedules subscription updates internally.
So, if you really need to run effects without returning them from an action, that's how you could do it. Just be aware that Hyperapp does not intentionally support this usage.
Almost Done
We have discussed how to divide-and-conquer the view with view components, the business-logic with primitive transforms – and even the wiring of it all with getters, setters and mapped transforms. The final pieces and some helpful closing words await you in part 7.
Top comments (0)