Nesting
Act encourages you to build simple apps, but having to reuse parts of your app will be very common. For this, one of the techniques you can use is nesting. If you read the Elm architecture documentation you already have a good idea of what I am talking about.
Let's go over a multi-counter example. First, let's implement the counter:
// counter.js
const view = (count) =>
['.counter', [
['h1', count],
['button', {click: {add: 1}}, 'Add 1'],
['button', {click: {add: -1}}, 'Remove 1']
]]
export const model = 0
export const reducer = (state, {type, payload}) =>
type === 'add' ? state + payload : state
export default { view, model, reducer }
Note the only difference here from a counter app is that you don't need to
import and use main
. You simply export the relevant parts of a counter,
namely view
, model
and reducer
.
If you wanted to just render this counter, it would be as easy as:
import counter from './counter'
import main from '@act/main'
const { view, model, reducer } = counter
main(view, { model, reducer })
But we want to nest two counters togheter. To accomplish this, we'll import the
nest
helpers:
// twoCounters.js
import counter from './counter'
import nest from '@act/main/nest'
const view = ({top, bottom}) => (
['main', [
nest.view('top', counter.view(top)),
nest.view('bottom', counter.view(bottom))
]]
)
const reducer = nest.reducers({
top: counter.reducer,
bottom: counter.reducer
})
const model = {
top: counter.model,
bottom: counter.model
}
export default { view, model, reducer }
As you can see, nest
give us two methods. The first, nest.view
receives a
string and a view JSON. What it does is that it renders the view, changing all
events to prefix the given string in the type of it's actions. Yes, it's a bit
magical. This means that the {click: {add: 1}}
will be transformed to
{click: {'top.add': 1}}
and {click: {'bottom.add': 1}}
. This is because we
need a way to differentiate events that happen in different elements. You could
simply change your reducers to account for this new action type. But before you
do that, let's see the second helper.
The second helper, nest.reducers
, as expected will create a new reducer the
accounts for the new action types injected in the views.
So if the counter reducer is...
export const reducer = (state, {type, payload}) =>
type === 'add' ? state + payload : state
... when we do ...
const reducer = nest.reducers({
top: counter.reducer,
bottom: counter.reducer
})
...we will end up with something like this:
export const reducer = (state, {type, payload}) => {
switch (type) {
case 'top.add':
return { ...state, top: state + payload }
case 'bottom.add':
return { ...state, bottom: state + payload }
default:
return state
}
}
Pretty rad, hum? But you probably noticed we're still exporting the nested counters. That's because nesting is recursive, supporting any level of nesting. So let's nest our counters one more time.
import twoCounters from './twoCounters'
import main from '@act/main'
import nest from '@act/main/nest'
const view = ({left, right}) => (
['main', [
['.left', {style: 'float: left'}, [
nest.view('left', twoCounters.view(left))
]],
['.right', {style: 'float: right'}, [
nest.view('right', twoCounters.view(right))
]]
]]
)
const reducer = nest.reducers({
left: twoCounters.reducer,
right: twoCounters.reducer
})
const model = {
left: twoCounters.model,
right: twoCounters.model
}
main(view, model, reducer)
Now our model will be a deeply nested object:
{
left: {
top: 0,
bottom: 0
},
right: {
top: 0,
bottom: 0
}
}
Our views will emit actions with types like left.top.add
and
right.bottom.add
, and our reducer will handle them nicely.
Notice most of the time you'll have more data than just a nested group of components. You have two options to deal with that. First, you can always just use the nested reducer as part of another reducer:
const countersReducer = nest.reducers({
left: twoCounters.reducer,
right: twoCounters.reducer
})
const reducer = (state, { type, payload }) => {
switch (type) {
case 'someOtherAction:
...
default:
return countersReducer(state, { type, payload })
}
}
The other alternative is to use combine
, to combine reducers. This method is
analogous to Redux's combineReducers
.