Composing components with PureScript Pux
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
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
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
- Increment
- Decrement
- 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
- Import
Network.HTTP.Affjax (AJAX)
Add AJAX to the AppEfects type
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
Links
- PureScript By Example. The PureScript book
- Pux Tutorial
- 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
- Haskell Programming from first principles. Invaluable if you are new to PureScript/Haskell
- Source code for this example. Diff on d621094abb4cb3a9bea2b1a831d50fbc5ef014a6 will show changes made for this post.