Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
205 lines (161 sloc) 7.69 KB

Handling effects

⚠️ Please note, this is the guide for Halogen 4 ⚠️

If you're interested in the new stuff, take a look at the changes in v5 document. If v4 is what you want, switch to browsing the repository at that tag to ensure you're seeing the right code and examples.


Halogen components have no built-in mechanisms for dealing with effects during query evaluation. That doesn't mean that they can't have effects, only that there is no implicit mechanism for them. They're made explicit in the usual way: via the type signature.

Let's take another look at the type of the button component from the last chapter:

myButton :: forall m. H.Component HH.HTML Query Input Message m

The m parameter we left polymorphic here is our means of introducing effect handling into a component eval function.

Using Effect during eval

Here's a component that generates a random number on demand and displays it to the user:

import Prelude
import Effect.Aff (Aff)
import Effect.Random (random)
import Data.Maybe (Maybe(..), maybe)
import Halogen as H
import Halogen.HTML as HH
import Halogen.HTML.Events as HE

type State = Maybe Number

data Query a = Regenerate a

ui :: H.Component HH.HTML Query Unit Void Aff
ui =
  H.component
    { initialState: const initialState
    , render
    , eval
    , receiver: const Nothing
    }
  where

  initialState :: State
  initialState = Nothing

  render :: State -> H.ComponentHTML Query
  render state =
    let
      value = maybe "No number generated yet" show state
    in
      HH.div_ $
        [ HH.h1_ [ HH.text "Random number" ]
        , HH.p_ [ HH.text ("Current value: " <> value) ]
        , HH.button
            [ HE.onClick (HE.input_ Regenerate) ]
            [ HH.text "Generate new number" ]
        ]

  eval :: Query ~> H.ComponentDSL State Query Void Aff
  eval = case _ of
    Regenerate next -> do
      newNumber <- H.liftEffect random
      H.put (Just newNumber)
      pure next

A runnable version of this is available in the effects-eff-random example.

To be able to use random, we've populated the m type variable with an Aff. This needs applying to both the component and eval function:

ui :: H.Component HH.HTML Query Unit Void Aff

eval :: Query ~> H.ComponentDSL State Query Void Aff

Why are we using Aff rather than Effect? For convenience - when it's time to run our UI, Halogen expects an Aff here. It is possible to hoist a component and change the m type, but it's easier if we just use Aff in the first place. Aff can do anything Effect can, so we're not losing out, just admitting more possibilities than we might need.

We can now use the liftEffect function in eval:

eval = case _ of
  Regenerate next -> do
    newNumber <- H.liftEffect random
    H.put (Just newNumber)
    pure next

This works as there's a MonadEffect instance for HalogenM for any m that also has a MonadEffect instance, and Aff satisfies this constraint. (As a reminder, ComponentDSL is an synonym for HalogenM).

Using Aff during eval

Occasionally it's useful to be able to fetch data from an API, so let's use that for the next example. We're going to make use of the affjax library as it provides a nice Aff-based interface for AJAX requests. Our data source will be GitHub's user API.

import Prelude
import Data.Maybe (Maybe(..))
import Effect.Aff (Aff)
import Halogen as H
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.HTML.Properties as HP
import Network.HTTP.Affjax as AX
import Network.HTTP.Affjax.Response as AXResponse

type State =
  { loading :: Boolean
  , username :: String
  , result :: Maybe String
  }

data Query a
  = SetUsername String a
  | MakeRequest a

ui :: H.Component HH.HTML Query Unit Void Aff
ui =
  H.component
    { initialState: const initialState
    , render
    , eval
    , receiver: const Nothing
    }
  where

  initialState :: State
  initialState = { loading: false, username: "", result: Nothing }

  render :: State -> H.ComponentHTML Query
  render st =
    HH.form_ $
      [ HH.h1_ [ HH.text "Lookup GitHub user" ]
      , HH.label_
          [ HH.div_ [ HH.text "Enter username:" ]
          , HH.input
              [ HP.value st.username
              , HE.onValueInput (HE.input SetUsername)
              ]
          ]
      , HH.button
          [ HP.disabled st.loading
          , HE.onClick (HE.input_ MakeRequest)
          ]
          [ 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 ] ]
              ]
      ]

  eval :: Query ~> H.ComponentDSL State Query Void Aff
  eval = case _ of
    SetUsername username next -> do
      H.modify_ (_ { username = username, result = Nothing :: Maybe String })
      pure next
    MakeRequest next -> do
      username <- H.gets _.username
      H.modify_ (_ { loading = true })
      response <- H.liftAff $ AX.get AXResponse.string ("https://api.github.com/users/" <> username)
      H.modify_ (_ { loading = false, result = Just response.response })
      pure next

A runnable version of this is available in the effects-aff-ajax example.

As with the Effect-based example, we've populated the m type variables with Aff. This time we're going to rely on the MonadAff instance and use liftAff:

MakeRequest next -> do
  username <- H.gets _.username
  H.modify_ (_ { loading = true })
  response <- H.liftAff $ AX.get AXResponse.string ("https://api.github.com/users/" <> username)
  H.modify_ (_ { loading = false, result = Just response.response })
  pure next

Note how there was no need to setup callbacks or anything of that nature. Using liftAff means we can mix the behaviour of Aff with our other component-related operations, giving us seamless async capabilities.

Mixing Effect and Aff

Any type that satisfies a MonadAff constraint also satisfies MonadEffect, so using Aff as the base monad for a component allows liftEffect and liftAff to be used together freely.

Let's take a look at running a component to produce a UI next.

You can’t perform that action at this time.