FantasyX aims to reduce all verbose code when you create and compose React Component in functional pattern. The idea is to define the logic of the program and then apply to a React Stateless Component at the last minute.

The idea of FantasyX is highly inspired by flare by purescript

Example 0: How to use xReact Fantasy

To get start with FantasyX, simply import Typeclasses you need, apart from simple Typeclasses, there’re two data type you should notice: FantasyX and Xstream.

FantasyX is a Applicative while Xstream is a Monad

which means Xstream can be flatMap and convert to FantasyX at any time.

import * as React from 'react';
import { render } from 'react-dom';
import 'xreact/lib/rx'
import { X, Applicative, lift2,Semigroup, Functor, map, Traversable, FlatMap } from 'xreact'
function xmount(component, dom) { render(React.createFactory(X)({}, component), dom) }

Example 1: Two number multiply

Now let’s see what FantasyX is with a simple example.

Since FantasyX is a Applicative.

A multiply function can be simply lifted to be a function that works with FantasyX

let mult = (x:number,y: number) => x * y
let xmult = lift2<"FantasyX",number, number, number>(mult)
let Xeg1 = xmult(Applicative.FantasyX.pure(6), Applicative.FantasyX.pure(5))

Now we need a A very basic Stateless Component to display the result of multiply.

let ViewEg1 = props => <p className="result">{props.product}</p>

Map the result to the form View the recognize, and apply to it. Now we have a React Component to mount

let Eg1 = Functor.FantasyX.map(a=>({product: a}), Xeg1).apply(ViewEg1)

using xmount to mount it to the DOM

xmount(<Eg1/>, document.getElementById('eg1') )

It’s too simple and no interacting anywhere, but you may get some idea of FantasyX Applicative

Example 2: Two Inputs

Now let’s make an interacting example.

Xstream can be viewed as a bridge that connect Component and FantasyX, you can make a Xstream from Event, which trigger by {props.actions.fromEvent} be called by input component.

 1  import {Xstream} from '../../../../../src/fantasy/xstream';
 2  
 3  function strToInt(x) {return ~~x}
 4  
 5  
 6  let XSinput1 = Xstream.fromEvent('change', 'n1', '5') // <- 1
 7  let XSinput2 = Xstream.fromEvent('change', 'n2', '6')
 8  
 9  let Xeg2 = lift2<"Xstream", number, number, number>(mult)( // <- 2
10    Functor.Xstream.map(strToInt, XSinput1),
11    Functor.Xstream.map(strToInt, XSinput2)
12  ).toFantasyX() // <- 3
13      .map(x=>({product: x}))
14  
15  let ViewEg2 = props => <section>
16      <p><input type="number" name="n1" onChange={props.actions.fromEvent} defaultValue="5"/></p>
17      <p><input type="number" name="n2" onChange={props.actions.fromEvent} defaultValue="6"/></p>
18      <p><span className="result">{props.product}</span></p>
19      </section>
20  
21  let Eg2 = Xeg2.apply(ViewEg2)
  1. line 6: create 2 streams from event
  2. line 9: lift mult to function that can apply to Xstream
  3. line 12: from Xstream to FantasyX

Example 3: Semigroup

Xstream and FantasyX are also Semigroup, which means they are concatable.

 1  let Xeg3 = Semigroup.Xstream.concat( // <- 1
 2    Semigroup.Xstream.concat(
 3      Xstream.fromEvent('change', 'firstName', 'Jichao'), // <- 2
 4      Applicative.Xstream.pure(' ')
 5    ),
 6    Xstream.fromEvent('change', 'lastName', 'Ouyang')
 7  ).toFantasyX()
 8  
 9  
10  let ViewEg3 = props => <section>
11      <p><input type="text" name="firstName" onChange={props.actions.fromEvent} defaultValue="Jichao" /></p>
12      <p><input type="text" name="lastName" onChange={props.actions.fromEvent} defaultValue="Ouyang"/></p>
13      <p><span className="result">{props.semigroup}</span></p>
14      </section>
15  
16  let Eg3 = Xeg3.map(a=>({semigroup: a})).apply(ViewEg3)
  1. line 1: JS don’t have custom operator, otherwise it should be same as Xstream.fromEvent('change', 'firstName', 'Jichao') + Applicative.Xstream.pure(' ') + Xstream.fromEvent('change', 'lastName', 'Ouyang')
  2. line 3: since string is Semigroup as well, so the result will be 'Jichao' + ' ' + 'Ouyang'

Example 4: Traverse

img

Traverse basically traverse a function to functor into a traversable, and then wrap the result of traversable into the functor.

For example we have a list of numbers, we can simply sum them into one number.

But what if I have a function can turn number into a Functor number, which in our case is a Xstream of number value. How can we sum the value of Functors.

In our case, the function from number -> Xstream number is in line 6

we traverse the function to a list of numbers in line 5

 1  function sum(list){
 2    return list.reduce((acc,x)=> acc+x, 0)
 3  }
 4  let list = ['1', '2', '3', '4', '5', '6', '7']
 5  let Xeg4 = Traversable.Array.traverse<'Xstream', string, string>('Xstream')( // <-
 6      (defaultVal, index) => (Xstream.fromEvent('change', 'traverse' + index, defaultVal)), // <-
 7      list
 8  ).toFantasyX()
 9      .map(xs => xs.map(strToInt))
10      .map(sum)
11  
12  let ViewEg4 = props => <section>
13  {list.map((item, index) => (<p>
14  <input key={index} type="number" name={"traverse" + index} onChange={props.actions.fromEvent} defaultValue={item} />
15  </p>))
16  }
17    <p><span className="result">{props.sum}</span></p>
18  </section>
19  
20  let Eg4 = Xeg4.map(a=>({sum: a})).apply(ViewEg4)

Then we will get a Function of numbers, Xstream<Array<number>>

You’ll know what to do next, just map the sum function to the functor, yay.

Example 5: Asynchronous

You know how complicated handling Async Action for react if you come from redux/saga or so, it’s too verbose to be reason about. But since we have Monad instance of Xstream, async is now just as easy as a flatMap

To give you a brief idea of how it works, I’ll implement a BMI Calculator, in just 30 lines of code. Imagine how many lines of code will it be if you involve redux and saga to handle async action.

  • bmiCalc function is a regular function that returns a Promise. It just send request to an API and fulfill the result to Promise
  • xweight and xheight will turn events into Xstream
  • promiseXstream will lift bmiCalc to Xstream and apply to xweight and xheight, then it will return a Xstream<Promise<object>>

What should we do with the promise?

since you can simply create a Xstream from Promise with fromPromise method, flatMap it to promiseXstream will give us a regular Xstream<object> type.

 1    function bmiCalc(weight, height) {
 2      return fetch(`https://gist.github.com.ru/jcouyang/edc3d175769e893b39e6c5be12a8526f?height=${height}&weight=${weight}`)
 3        .then(resp => resp.json())
 4        .then(json => json.result)
 5    }
 6  
 7    let xweigth = Xstream.fromEvent('change', 'weight', '70') // <-
 8    let xheight = Xstream.fromEvent('change', 'height', '175') // <-
 9  
10    let promiseXstream = lift2<"Xstream", string, string, Promise<any>>(bmiCalc)( // <-
11      xweigth,
12      xheight
13    )
14  
15    let Xeg5 = FlatMap.Xstream.flatMap(Xstream.fromPromise, promiseXstream) // <-
16        .toFantasyX()
17  
18    let ViewEg5 = props => (
19        <div>
20        <label>Height: {props.height} cm
21        <input type="range" name="height" onChange={props.actions.fromEvent} min="150" max="200" defaultValue={props.height} />
22        </label>
23        <label>Weight: {props.weight} kg
24        <input type="range" name="weight" onChange={props.actions.fromEvent} min="40" max="100" defaultValue={props.weight} />
25        </label>
26        <p>HEALTH: <span>{props.health}</span></p>
27        <p>BMI: <span className="result">{props.bmi}</span></p>
28        </div>
29    )
30  
31  let Eg5 = Xeg5.merge(xweigth.toFantasyX().map(w=>({weight:w})))
32      .merge(xheight.toFantasyX().map(h=>({height:h})))
33      .apply(ViewEg5)

Example 6: Fold

FantasyX is also “Foldable”, not official Typeclass Foldable, it fold on react state, so it’s named foldS.

let Xeg6 = Xstream.fromEvent('click', 'increment')
    .toFantasyX<{count:number}>()
    .map(x => 1)
    .foldS((acc, a) => {
      return { count: (acc.count||0) + a }})

let ViewEg6 = props => <p>
    <span className="result">{props.count || 0}</span>
    <input type="button" name="increment" value="+1" onClick={e=>props.actions.fromEvent(e)} />
    </p>

let Eg6 = Xeg6.apply(ViewEg6)

With foldS and toStream, it’s very easy to unit test FantasyX

Example 7: Merge

Two FantasyX can be merged into one, with both behavior of those two.

let Xeg7 = Xstream.fromEvent('click', 'decrement')
      .toFantasyX<{count:number}>()
      .map(x => -1)
      .foldS((acc, a) => {
        return { count: (acc.count||0) + a }})

  let ViewEg7 = props => <p>
      <input type="button" name="decrement" value="-" onClick={props.actions.fromEvent} />
      <span className="result">{props.count || 0}</span>
      <input type="button" name="increment" value="+" onClick={props.actions.fromEvent} />
  </p>

  let Eg7 = Xeg7.merge(Xeg6).apply(ViewEg7)

Example 8: Fold multiple buttons

Simply switch on different actions and fold.

const actions = ['-1', '+1', 'reset']
let Xeg8 =
  actions.map((action)=>Xstream.fromEvent('click', action).toFantasyX<{count:number}>())
    .reduce((acc,a)=>acc.merge(a))
    .foldS((acc, i) => {
    acc.count = acc.count || 0
      switch(i) {
      case '-1': return {count: acc.count -1}
      case '+1': return {count: acc.count +1}
      case 'reset': return {count: 0}
      default: acc
      }
    }
)

let ViewEg8 = props => <p>
  <span className="result">{props.count}</span>
  {actions.map(action=>
    <input type="button" name={action} value={action} onClick={props.actions.fromEvent} />)}
</p>

let Eg8 = Xeg8.apply(ViewEg8)

xmount(<Eg8/>, document.getElementById('eg8') )