Performing Effects
We've covered a lot of ground so far. You know how to write Halogen HTML. You can define components that respond to user interactions and model each part of the component in types. With this foundation we can move on to another vital tool when writing applications: performing effects.
In this chapter we'll explore how to perform effects in your component through two examples: generating random numbers and making HTTP requests. Once you know how to perform effects you are well on your way to mastering Halogen fundamentals.
Before we start, it's important to know that you can only perform effects during evaluation, which means functions like handleAction
which use the type HalogenM
. You can't perform effects when you produce your initial state or during rendering. Since you can only perform effects when you're within HalogenM
, let's briefly learn more about it before diving in to the examples.
The HalogenM
Type
If you recall from last chapter, the handleAction
function returns a type called HalogenM
. Here's the handleAction
we wrote:
handleAction :: forall output m. Action -> HalogenM State Action () output m Unit
HalogenM
is a crucial part of Halogen, often called the "eval" monad. This monad enables Halogen features like state, forking threads, starting subscriptions, and more. But it's quite limited, concerning itself only with Halogen-specific features. In fact, Halogen components have no built-in mechanisms for effects!
Instead, Halogen lets you choose what monad you would like to use with HalogenM
in your component. You gain access to all the capabilities of HalogenM
and also whatever capabilities your chosen monad supports. This is represented with the type parameter m
, which stands for "monad".
A component that only uses Halogen-specific features can leave this type parameter open. Our counter, for example, only updated state. But a component that performs effects can use the Effect
or Aff
monads, or you can supply a custom monad of your own.
This handleAction
is able to use functions from HalogenM
like modify_
and can also use effectful functions from Effect
:
handleAction :: forall output. Action -> HalogenM State Action () output Effect Unit
This one can use functions from HalogenM
and also effectful functions from Aff
:
handleAction :: forall output. Action -> HalogenM State Action () output Aff Unit
It is more common in Halogen to use constraints on the type parameter m
to describe what the monad can do rather than choose a specific monad, which allows you to mix several monads together as your application grows. For example, most Halogen apps would use functions from Aff
via this type signature:
handleAction :: forall output m. MonadAff m => Action -> HalogenM State Action () output m Unit
This lets you do everything the hardcoded Aff
type did, but it also lets you mix in other constraints too.
One last thing: when you choose a monad for your component it will show up in your HalogenM
type, your Component
type, and, if you are using child components, in your ComponentHTML
type:
component :: forall query input output m. MonadAff m => H.Component query input output m
handleAction :: forall output m. MonadAff m => Action -> HalogenM State Action () output m Unit
-- We aren't using child components, so we don't have to use the constraint here, but
-- we'll learn about when it's required in the parent & child components chapter.
render :: forall m. State -> H.ComponentHTML Action () m
An Effect
Example: Random Numbers
Let's create a new, simple component that generates a new random number each time you click a button. As you read through the example, notice how it uses the same types and functions that we used to write our counter. Over time you'll become used to scanning the state, action, and other types of a Halogen component to get a gist of what it does, and familiar with standard functions like initialState
, render
, and handleAction
.
You can paste this example into Try Purescript to explore it interactively. You can also see and run the full example code from the
examples
directory in this repository.
Notice that we don't perform any effects in our initialState
or render
functions -- for example, we initialize our state to Nothing
rather than generate a random number for our initial state -- but we're free to perform effects in our handleAction
function (which uses the HalogenM
type).
module Main where
import Prelude
import Data.Maybe (Maybe(..), maybe)
import Effect (Effect)
import Effect.Class (class MonadEffect)
import Effect.Random (random)
import Halogen as H
import Halogen.Aff (awaitBody, runHalogenAff)
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.VDom.Driver (runUI)
main :: Effect Unit
main = runHalogenAff do
body <- awaitBody
runUI component unit body
type State = Maybe Number
data Action = Regenerate
component :: forall query input output m. MonadEffect m => H.Component query input output m
component =
H.mkComponent
{ initialState
, render
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
}
initialState :: forall input. input -> State
initialState _ = Nothing
render :: forall m. State -> H.ComponentHTML Action () m
render state = do
let value = maybe "No number generated yet" show state
HH.div_
[ HH.h1_
[ HH.text "Random number" ]
, HH.p_
[ HH.text ("Current value: " <> value) ]
, HH.button
[ HE.onClick \_ -> Regenerate ]
[ HH.text "Generate new number" ]
]
handleAction :: forall output m. MonadEffect m => Action -> H.HalogenM State Action () output m Unit
handleAction = case _ of
Regenerate -> do
newNumber <- H.liftEffect random
H.modify_ \_ -> Just newNumber
As you can see, a component that performs effects is not much different from a component that doesn't! We've only done two things:
- We added a
MonadEffect
constraint to them
type parameter for our component and for ourhandleAction
function. We don't need the constraint for our render function because we don't have any child components. - We actually used an effect for the first time: the
random
function, which comes fromEffect.Random
.
Let's break down using this effect a little more.
-- [1]
handleAction :: forall output m. MonadEffect m => Action -> H.HalogenM State Action () output m Unit
handleAction = case _ of
Regenerate -> do
newNumber <- H.liftEffect random -- [2]
H.modify_ \_ -> Just newNumber -- [3]
- We have constrained our
m
type parameter to say we support any monad, so long as that monad supportsMonadEffect
. It's another way to say "We need to be able to useEffect
functions in our evaluation code." - The
random
function has the typeEffect Number
. But we can't use it directly: our component doesn't supportEffect
but rather any monadm
so long as that monad can run effects fromEffect
. It's a subtle difference, but in the end we require therandom
function to have the typeMonadEffect m => m Number
instead of beingEffect
directly. Fortunately, we can convert anyEffect
type toMonadEffect m => m
using theliftEffect
function. This is a common pattern in Halogen, so keepliftEffect
in mind if you're usingMonadEffect
. - The
modify_
function lets you update state, and it comes directly fromHalogenM
with the other state update functions. Here we use it to write the new random number to our state.
This is a nice example of how you can freely interleave effects from Effect
with Halogen-specific functions like modify_
. Let's do it again, this time using the Aff
monad for asynchronous effects.
An Aff
Example: HTTP Requests
It's common to fetch information from elsewhere on the Internet. For example, let's say we'd like to work with GitHub's API to fetch users. We'll use the affjax
package to make our requests, which itself relies on the Aff
monad for asynchronous effects.
This example is even more interesting, though: we'll also use the preventDefault
function to prevent form submission from refreshing the page, which runs in Effect
. That means our example shows how you can interleave different effects together (Effect
and Aff
) along with Halogen functions (HalogenM
).
As with the Random example, you can paste this example into Try Purescript to explore it interactively. You can also see and run the full example code from the
examples
directory in this repository.
This component definition should start to look familiar. We define our State
and Action
types and implement our initialState
, render
, and handleAction
functions. We bring them together into our component spec and turn them into a valid component H.mkComponent
.
Once again, notice that our effects are concentrated in the handleAction
function and no effects are performed when making the initial state or rendering Halogen HTML.
module Main where
import Prelude
import Affjax.Web as AX
import Affjax.ResponseFormat as AXRF
import Data.Either (hush)
import Data.Maybe (Maybe(..))
import Effect (Effect)
import Effect.Aff.Class (class MonadAff)
import Halogen as H
import Halogen.Aff (awaitBody, runHalogenAff)
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.HTML.Properties as HP
import Halogen.VDom.Driver (runUI)
import Web.Event.Event (Event)
import Web.Event.Event as Event
main :: Effect Unit
main = runHalogenAff do
body <- awaitBody
runUI component unit body
type State =
{ loading :: Boolean
, username :: String
, result :: Maybe String
}
data Action
= SetUsername String
| MakeRequest Event
component :: forall query input output m. MonadAff m => H.Component query input output m
component =
H.mkComponent
{ initialState
, render
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
}
initialState :: forall input. input -> State
initialState _ = { loading: false, username: "", result: Nothing }
render :: forall m. State -> H.ComponentHTML Action () m
render st =
HH.form
[ HE.onSubmit \ev -> MakeRequest ev ]
[ HH.h1_ [ HH.text "Look up GitHub user" ]
, HH.label_
[ HH.div_ [ HH.text "Enter username:" ]
, HH.input
[ HP.value st.username
, HE.onValueInput \str -> SetUsername str
]
]
, HH.button
[ HP.disabled st.loading
, HP.type_ HP.ButtonSubmit
]
[ HH.text "Fetch info" ]
, HH.p_
[ HH.text $ if st.loading then "Working..." else "" ]
, HH.div_
case st.result of
Nothing -> []
Just res ->
[ HH.h2_
[ HH.text "Response:" ]
, HH.pre_
[ HH.code_ [ HH.text res ] ]
]
]
handleAction :: forall output m. MonadAff m => Action -> H.HalogenM State Action () output m Unit
handleAction = case _ of
SetUsername username -> do
H.modify_ _ { username = username, result = Nothing }
MakeRequest event -> do
H.liftEffect $ Event.preventDefault event
username <- H.gets _.username
H.modify_ _ { loading = true }
response <- H.liftAff $ AX.get AXRF.string ("https://api.github.com/users/" <> username)
H.modify_ _ { loading = false, result = map _.body (hush response) }
This example is especially interesting because:
- It mixes together functions from multiple monads (
preventDefault
isEffect
,AX.get
isAff
, andgets
andmodify_
areHalogenM
). We're able to useliftEffect
andliftAff
along with our constraints to make sure everything plays well together. - We only have one constraint,
MonadAff
. That's because anything that can be run inEffect
can also be run inAff
, soMonadAff
impliesMonadEffect
. - We're making multiple state updates in one evaluation.
That last point is especially important: when you modify state your component renders. That means that during this evaluation we:
- Set
loading
totrue
, which causes the component to re-render and display "Working..." - Set
loading
tofalse
and update the result, which causes the component to re-render and display the result (if there was one).
It's worth noting that because we're using MonadAff
our request will not block the component from doing other work, and we don't have to deal with callbacks to get this async superpower. The computation we've written in MakeRequest
simply suspends until we get the response and then proceeds to update the state the second time.
It's a smart idea to only modify state when necessary and to batch updates together if possible (like how we call modify_
once to update both the loading
and result
fields). That helps make sure you're only re-rendering when needed.
Event Handling Revisited
There is a lot going on in this example, so it is worth concentrating for a moment on the new event-handling features which it introduces. Unlike the simple click handlers of the button example, the handlers defined here do make use of the event data they are given:
- The value of the username input is used by the
onValueInput
handler (theSetUsername
action). preventDefault
is called on the event in theonSubmit
handler (theMakeRequest
action).
The type of parameter passed to the handler depends on which function is used to attach it. Sometimes, as for onValueInput
, the handler simply receives data extracted from the event - a String
in this case. Most of the other on...
functions set up a handler to receive the whole event, either as a value of type Event
, or as a specialised type like MouseEvent
. The details can be found in the module documentation for Halogen.HTML.Events
on pursuit; the types and functions used for events can be found in the web-events and web-uievents packages.