Package

purescript-rx-state

Repository
jasonzoladz/purescript-rx-state
License
MIT
Uploaded by
jasonzoladz

A tiny library for unidirectional data flow in PureScript applications using RxJS. (tl;dr: Erik Meijer has already solved your state management problems.)

As this library relies on RxJS, you'll need to npm install rx.

(Note: I've deliberately not taken a dependency on purescript-rx. It's a very nice wrapper (and you should use it), but I've wrapped a couple RxJS functions that don't line-up with the types defined in purescript-rx. There are no conflicts, however. You can use it alongside this.)

####Install

npm (or bower) install rx or use a CDN

bower install purescript-rx-state

####Usage

It's dead simple. The API is very similar to startApp in Elm.

First, define your State. It must be some record type, like:

type State = { num :: Int }

Then define some actions and effects:

data Action
  = Increment
  | Decrement
  | NoOp

data Effect
  = AjaxIncrement
  | NoFx

Next, define Channels for your Actions and Effects.
Note: If you want to use startApp, your actions and effects Channels must carry a Foldable. Most likely, you'll use an Array, like so:

actionsChannel :: Channel (Array Action)
actionsChannel = newChannel []

effectsChannel :: Channel (Array Effect)
effectsChannel = newChannel []

If you need to kickoff any ajax requests when the app starts, then simply put them in your initial effectsChannel declaration like this:

effectsChannel :: Channel (Array Effect)
effectsChannel = newChannel [ AjaxIncrement ]

Now, define your update function. Your update function takes a State, and an Action, and returns a new State.

update :: State -> Action -> State
update state action =
  case action of
    Increment -> state { num = state.num + 1 }
    Decrement -> state { num = state.num - 1 }
    NoOp      -> state

And define your function that performs (possibly asynchronous) effects. If an asynchronous Effect returns a payload, you simply dispatch the payload as a part of another Action.

performEffect :: forall e. Effect -> Eff ( console :: CONSOLE, ajax :: AJAX | e) Unit
performEffect fx =
  case fx of
    AjaxIncrement -> runAff
                            (\_ -> send [ Decrement ] actionsChannel)
                            (\_ -> send [ Increment ] actionsChannel)
                            ((affjax $ defaultRequest { url = "http://jsonplaceholder.typicode.com/posts/1", method = GET })

    NoFx              -> return unit

In your views, you can dispatch an Action or Effect by using the send function to put an Action or Effect value on the corresponding Channel. Again, note that (when using startApp) you are sending a Foldable Action/Foldable Effect, since you may want to dispatch multiple actions or effects at the same time.

If using purescript-react, you might do this:

hello :: ReactClass State
hello = createClass $ spec unit $ \ctx -> do
  state <- getProps ctx
  return $
    D.div [] [ D.h1 []
                  [ D.text "Hello, the state is: "
                  , D.text (show state.num)
                  ],
               D.div []
                  [ D.button [ P.onClick (\_ -> send [Increment] actionsChannel) ]
                             [ D.text "Increment" ]
                  , D.button [ P.onClick \_ -> send [Decrement] actionsChannel ]
                             [ D.text "Decrement" ]
                  , D.button [ P.onClick \_ -> send [AjaxIncrement] effectsChannel ]
                             [ D.text "Ajax Increment" ]
                  ]

Finally, if you wish, wire it up in main using startApp.

startApp has the following type signature.

startApp :: forall eff state action effect view f. (Foldable f)
                                                => (state -> action -> state) -- Your "update" function
                                                -> (effect -> Eff eff Unit) -- Your "effects" function
                                                -> (state -> Eff eff view) -- Your "render" function
                                                -> Channel (f action) -- Your "actions" channel
                                                -> Channel (f effect) -- Your "effects" channel
                                                -> state -- Your initial state
                                                -> Eff eff Unit

startApp isn't required though. foldp (an alias for RxJS' scan function) is available if you want to wire the signal graph yourself. (This may be necessary if you want to map custom channels to actions and then merge them together -- for example, mapping a Channel Path to a Channel Action for single page app routing. Note: I plan to provide a startApp for single page routing in purescript-rx-state-utils. Stay tuned . . .)

Here, I'm using purescript-react for rendering but you could substitute any function with the same type as:

fooRender :: forall view eff.  State -> Eff eff view
main :: forall eff. Eff (dom :: DOM, console :: CONSOLE, ajax :: AJAX | eff ) Unit
main = startApp update performEffect myRender actionsChannel effectsChannel initState

  where
    view :: State -> ReactElement
    view appState = D.div' [ createFactory hello appState ]

    myRender state = do
      container <- elm'
      RD.render (view state) container

    elm' :: forall eff. Eff (dom :: DOM | eff) Element
    elm' = do
      win <- window
      doc <- document win
      elm <- getElementById (ElementId "app") (documentToNonElementParentNode (htmlDocumentToDocument doc))
      return $ fromJust (toMaybe elm)

A minimal, but complete, example is in the example repo.