Awesome
purescript-rx-state
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 Channel
s for your Action
s and Effect
s.
Note: If you want to use startApp, your actions and effects Channel
s 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 send
ing 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.