Modularity
In the basics of the architecture described a simple Counter
module. In this example that Counter
module will be used to create pair of counters. Each counter will be changeable independently, and the reset button will reset both counters back to 0
.
The CounterPair
module starts with importing the Counter
module and the reflex library:
import {html, forward, start} from "reflex"
import type {DOM, Address} from "reflex"
import * as Counter from "./Counter"
While Counter.Model
, Counter.init
, etc. are used no knowledge is assumed in regards how they work. This is very subtle but important detail that enables true modularity.
Model
Counter
module already defines a complete representation of a counter via Counter.Model
type. There for modeling a pair of counters is pretty straight forward:
export class Model {
top: Counter.Model;
bottom: Counter.Model;
constructor(top:Counter.Model, bottom:Counter.Bottom) {
this.top = top
this.bottom = bottom
}
}
Model just needs to hold representation of top counter and a bottom counter. As of init
function it just needs to init
both of them:
export const init = (top:number=0, bottom:number=0) =>
new Model(Counter.init(top), Counter.init(bottom))
The Counter
module provides an init
function to create a Counter.Model
. Only thing necessary is to delegate to Counter.init
for top and bottom counter and then package results into Model
.
Update
Next thing we need is to define what kinds of messages can be received. In requirements we mentioned ability to reset counters, so message for that & all the messages produced by the top and bottom counters:
export type Message =
| { type: "Reset" }
| { type: "Top", top: Counter.Message }
| { type: "Bottom", bottom: Counter.Message }
First Reset
message is pretty normal. The of Top
and Bottom
cases are a bit different. They are just saying we will get counter messages from the top and bottom counters.
Important thing is there is no need to know anything about Counter.Message
, all we care about is weather it came from a top or a bottom counter.
Note: Due to an issue #1890 in flow, it is recommended to use distinct property names when defining types with properties of external type. Which is why
top: Counter.Message
andbottom: Counter.Message
are used here.
export const update = (model:Model, message:Message):Model => {
switch (message.type) {
case "Reset":
return init(0, 0)
case "Top":
const top = Counter.update(model.top, message.top)
return new Model(top, model.bottom)
case "Bottom":
const bottom = Counter.update(model.bottom, message.bottom)
return new Model(model.top, bottom)
}
}
When Reset
message is received, module is just reinitialized. When Top
message is received, there is only one thing that can be done. The Counter.update
function is the only function exposed by Counter
that takes an argument of Counter.Message
type (whatever that may be). So we just delegate to Counter.update
with a top counter, which gives us a new Counter.Model
which is placed in place of former top counter model. Exact same thing happens for the bottom counter.
What this means is that when someone adds a ton of new features to Counter
module in next three years, CounterPair
module will continue working exactly the same. All the details are encapsulated inside the Counter
module.
View
Finally, a view
function. Nothing new here, maybe just the fact top and bottom counters are passed "forwarding addresses" that tag messages before passing them on to address
so that update
can figure out which counter does wrapped message
belongs to.
export const view = (model:Model, address:Address<Message>):DOM =>
html.section({
className: "counter-pair"
}, [
html.div({
className: "top-counter"
}, [
Counter.view(model.top, forward(address, tagTop))
]),
html.div({
className: "bottom-counter"
}, [
Counter.view(model.bottom, forward(address, tagBottom))
]),
html.button({
onClick: forward(address, createReset)
}, ["Reset"])
])
const createReset = _ => ({ type: "Reset" })
const tagTop = message => ({ type: "Top", top: message })
const tagTop = message => ({ type: "Bottom", bottom: message })