-
Notifications
You must be signed in to change notification settings - Fork 25
FRP Best Practice
I’ll guide you to some good practice of Functional Programming through the Counter example.
It’s really helpful even you don’t use xreact, the FP idea is common and applicable to anywhere even for redux project.
union-type is a awesome library to define union type/case class.
most flux-like library will define Intents using the keyword `type`
inc: () => ({type: 'inc'})
but union type fit perfectly to define Intent
Intent.js
import Type from 'union-type'
export default Type({
Inc: []
Dec: []
})
it’s like case class in scala, you get a lot of benefit by using union-type Intent
import Intent from 'intent'
const counterable = x(intent$ => {
return {
sink$: intent$.map(Intent.case({
Inc: () => state => ({count: state.count + 1}),
Dec: () => state => ({count: state.count - 1}),
_: () => state => state
})),
actions: {
inc: Intent.Inc,
dec: Intent.Dec,
}
}
})
like scala, union type can also contain values
import Type from 'union-type'
export default Type({
Inc: [Number]
Dec: [Number]
})
if you define Intent constructors, you will be able to destruct them via case
import Intent from 'intent'
const counterable = x(intent$ => {
return {
sink$: intent$.map(Intent.case({
Inc: (value) => state => ({count: state.count + value}),
Dec: (value) => state => ({count: state.count - value}),
_: () => state => state
})),
actions: {
inc: Intent.Inc,
dec: Intent.Dec,
}
}
})
lens is composable, immutable, functional way to view, update your state
you can use lens
implemented by ramda, or update
in lodash
import {lens, over, inc, dec, identity} from 'ramda'
const counterable = x(intent$ => {
let lensCount = lens(prop('count'))
return {
sink$: intent$.map(Intent.case({
Inc: () => over(lensCount, inc)
Dec: () => over(lensCount, dec),
_: () => identity
}))
}
})
when the value is async, e.g. promise
it could be response from rest request, or other async IO
import when from 'when'
import {just, from, lens, over, set, inc, dec, identity, compose} from 'ramda'
const counterable = x(intent$ => {
let lensCount = lens(prop('count'))
return {
sink$: intent$.map(Intent.case({
Inc: () => over(lensCount, inc)
Dec: () => over(lensCount, dec),
_: () => identity
}))
data$: just(0)
.flatMap(compose(from, when)) // <-- when is a async value
.map(set(lensCount))
}
})
x wrappers are composable, just like functions
import Type from 'union-type'
export default Type({
Inc: [Number],
Dec: [Number],
Double: [],
Half: []
})
create a new wrapper with some kind of behaviors
const doublable = x(intent$ => {
let lensCount = lens(prop('count'))
return {
sink$: intent$.map(Intent.case({
Double: () => over(lensCount, x=>x*2)
Half: () => over(lensCount, x=>X/2),
_: () => identity,
}))
actions: {
double: Intent.Double,
half: Intent.Half,
}
}
})
compose doublable and increasable
const Counter = doublable(increasable(CounterView))
CounterView
then get both abilities of double/half and inc/dec
const CounterView = props => (
<div>
<button onClick={props.actions.half}>/2</button>
<button onClick={props.actions.dec}>-</button>
<span>{props.count}</span>
<button onClick={props.actions.inc}>+</button>
<button onClick={props.actions.double}>*2</button>
</div>
)
now our FRP counter example will become something like this