Skip to content
Jichao Ouyang edited this page Jun 12, 2017 · 7 revisions

Functional Best Practice in xReact

中文

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.

use Union Type to define Intent

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: []
})

case Intent, not switch

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,
        }
    }
})

pattern match case class

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

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
        }))
    }
})

flatMap

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))
    }
})

Composable wrapper

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

Clone this wiki locally