Composing components with PureScript Pux

Posted on August 15, 2016

PureScript Pux

Pux is a PureScript interface to React. The tutorial at http://www.alexmingoia.com/purescript-pux/index.html gives a good introduction to the library. The section titled Multiple components shows how you can easily compose simple components. What it does not show however is how to compose components that have effects (eff or aff).

I battled with this for a bit and so decided to document the solution in the hopes it helps other beginners. The explanation below shows how to compose components in the framework of the starter app.

I’m assuming that you have read the pux tutorial at least up to the fetching data section so I wont be covering the same detail here.

The starter app

The starter app has a counter component that is rendered in the Laout.purs file

The original counter

data Action = Increment | Decrement

type State = Int

init :: State
init = 0

update :: Action -> State -> State
update Increment state = state + 1
update Decrement state = state - 1

The original layout

data Action
  = Child (Counter.Action)
  | PageView Route

type State =
  { route :: Route
  , count :: Counter.State }

init :: State
init =
  { route: NotFound
  , count: Counter.init }

update :: Action -> State -> State
update (PageView route) state = state { route = route }
update (Child action) state = state { count = Counter.update action state.count }

Making the counter effectful

EffModel

The type of an update function with no effects (as above) is

update :: Action -> State -> State

For it to be effectful it should return an EffModel. The EffModel looks like this

type EffModel state action eff = 
    { state :: state
    , effects :: Array (Aff (channel :: CHANNEL | eff) action) 
    }

The update type will then be

update :: Action -> State -> EffModel State Action (dom :: DOM, ajax :: AJAX)

Changing the counter

To keep things simple I’ll make the counter component act as if its effectual without doing any actual IO. To perform effects the component returns a list of effects that should be performed in the EffModel’s effects. The counter needs to do three things

  1. Increment
  2. Decrement
  3. Apply the effects
update :: Action -> State -> EffModel State Action (dom :: DOM, ajax :: AJAX)
update (ReceiveInc i) state= 
  noEffects $ state + i
update Increment state =
  { state: state
  , effects: [ do
                  pure $ ReceiveInc 1
             ]
  }
update Decrement state =
  { state: state
  , effects: [ do
                  pure $ ReceiveInc (-1)
             ]
  }

view :: State -> Html Action
view state =
  div
    []
    [ button [ onClick (const Increment) ] [ text "Increment" ]
    , span [] [ text (show state) ]
    , button [ onClick (const Decrement) ] [ text "Decrement" ]
    ]

The increment and decrement cases return a ReceiveInc effect that will then be passed to the component and applied by the ReceiveInc case. In a real world component there would be an actual IO action e.g. using AJAX with affjax etc. But ultimately you are returning the effect in the array to be applied.

Changing the layout

The layout’s update function must also be changed to use an EffModel

update :: Action -> State -> EffModel State Action (dom :: DOM, ajax :: AJAX)
update (PageView route) state = noEffects $ state { route = route }
update (PostCounter action) state =

The problem is that the counter’s EffModel does not have the same type as the layout’s EffModel, so you can’t simply use the result from the child components. Rather you need to map both the state and the effects using the mapState and mapEffects functions.

update (PostCounter action) state =
  let efm = Counter.update action state.count in
  let st = mapState (\s -> state {count = s}) efm in
  let ef = mapEffects (\e -> PostCounter e) st in
  ef

So first the Counter’s update function is called. Then the state is mapped to change the state. Finally the effects are mapped by creating a PostCounter instance for each event. This can be cleaned up a bit, e.g. using the # operator

update (PostCounter action) state =
  Counter.update action state.count
  # mapState (state {count = _}) 
  # mapEffects PostCounter 

The final layout code looks like this

data Action
  = PostCounter (Counter.Action)
  | PostCounter2 (Counter.Action)
  | PageView Route

type State =
  { route :: Route
  , count :: Counter.State
  , count2 :: Counter.State
  }

init :: State
init =
  {
    route: NotFound
  , count: Counter.init
  , count2: Counter.init
  }

update :: Action -> State -> EffModel State Action (dom :: DOM, ajax :: AJAX)
update (PageView route) state = noEffects $ state { route = route }
update (PostCounter action) state =
  Counter.update action state.count
  # mapState (state {count = _}) 
  # mapEffects PostCounter 
update (PostCounter2 action) state =
  Counter.update action state.count2
  # mapState (state {count2 = _}) 
  # mapEffects PostCounter2 

view :: State -> Html Action
view state =
  div
    []
    [ h1 [] [ text "Pux Starter App" ]
    , p [] [ text "Change src/Layout.purs and watch me hot-reload." ]
    , case state.route of
        Home ->
          div
            []
            [
              map PostCounter $ Counter.view state.count
            , hr [] [] 
            , map PostCounter2 $ Counter.view state.count2
            ]
        NotFound -> NotFound.view state
    ]

Changing main

The only remaining changes are a few minor modifications to Main.purs

  1. Import Network.HTTP.Affjax (AJAX)
  2. Add AJAX to the AppEfects type

  3. The update function no longer needs the fromSimple

Summary

The full code for this example is available in this fork of the start app https://github.com/andrevdm/pux-starter-app-with-effects

  1. PureScript By Example. The PureScript book
  2. Pux Tutorial
  3. Pux blog sample project. Shows this and more in a real world project. This helped clear up some of the confusion for me, definitely worth taking a look at
  4. Haskell Programming from first principles. Invaluable if you are new to PureScript/Haskell
  5. Source code for this example. Diff on d621094abb4cb3a9bea2b1a831d50fbc5ef014a6 will show changes made for this post.