Halogen Documentation

Halogen is a declarative, type-safe library for building user interfaces.

This documentation covers how to use Halogen and provides a concepts reference. There are also other resources for learning and using Halogen, including:

Quick Start: Halogen Guide

If you are new to Halogen we recommend starting with the Halogen Guide. This short handbook demonstrates and explains Halogen concepts while building components.

By the end of the guide you'll be ready to dive in to more advanced resources like the Concepts Reference or Real World Halogen.

Going Deeper: Concepts Reference

Once you're comfortable with the main concepts from the Halogen Guide you may be interested in more advanced topics and in understanding why Halogen features are designed the way they are. The Concepts Reference will help you understand Halogen at a deeper level.

Major Version Changelog

Major Halogen releases are accompanied by guides for transitioning from one version to the next in the Major Version Changelog. Currently, there are transition guides for the following versions:

Halogen Guide

Halogen is a declarative, component-based UI library for PureScript that emphasizes type safety. In this guide you will learn the core ideas and patterns needed to write real-world applications in Halogen.

Here is a tiny Halogen app that lets you increment and decrement a counter:

module Main where

import Prelude

import Effect (Effect)
import Halogen as H
import Halogen.Aff as HA
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.VDom.Driver (runUI)

main :: Effect Unit
main = HA.runHalogenAff do
  body <- HA.awaitBody
  runUI component unit body

data Action = Increment | Decrement

component =
  H.mkComponent
    { initialState
    , render
    , eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
    }
  where
  initialState _ = 0

  render state =
    HH.div_
      [ HH.button [ HE.onClick \_ -> Decrement ] [ HH.text "-" ]
      , HH.div_ [ HH.text $ show state ]
      , HH.button [ HE.onClick \_ -> Increment ] [ HH.text "+" ]
      ]

  handleAction = case _ of
    Increment -> H.modify_ \state -> state + 1
    Decrement -> H.modify_ \state -> state - 1

You can paste this example (and any other full examples in this guide) into Try PureScript. We highly recommend doing this to explore the examples interactively! For example, try changing the buttons so they use the words "Increment" and "Decrement" instead of the symbols "+" and "-".

By default, Try PureScript will compile every time you make a change. You can also disable the auto-compile feature, which will cause Try PureScript to wait for you to click the "Compile" button to compile your Halogen application.

You can also create your own starter project with the official Halogen template. This template includes extra tools and scripts to help you get up and running with a full Halogen application.

Don't worry if this code is overwhelming at first -- when you've read the next few chapters of the guide you'll gain a solid understanding of how this component works and how to write your own.

How to Read This Guide

In this guide we'll explore the building blocks of Halogen apps: elements and components. When you understand these you can create complex apps from small, reusable pieces.

This is a step-by-step introduction to Halogen's main concepts. Each chapter builds on knowledge introduced in previous chapters, so we recommend reading through the guide in order.

Halogen is a PureScript library, and it assumes basic knowledge of PureScript concepts like functions, records, arrays, do notation, Effect, and Aff. It will also help if you understand the basics of HTML and the DOM. If you need a refresher, we recommend:

Table of Contents

  1. Rendering Halogen HTML
  2. Introducing Components
  3. Performing Effects
  4. Lifecycles & Subscriptions
  5. Parent & Child Components
  6. Running An Application
  7. Next Steps

Rendering Halogen HTML

Halogen HTML elements are the smallest building block of Halogen applications. These elements describe what you want to see on the screen.

Halogen HTML elements are not components (we'll get to components in the next chapter), and they can't be rendered without a component. However, it's common to write helper functions that produce Halogen HTML and then use those functions in a component.

We'll explore writing HTML without components or events in this chapter.

Halogen HTML

You can write Halogen HTML using functions from the Halogen.HTML or Halogen.HTML.Keyed modules as in this example:

import Halogen.HTML as HH

element = HH.h1 [ ] [ HH.text "Hello, world" ]

Halogen HTML elements can be thought of like browser DOM elements, but they are controlled by the Halogen library instead of being actual elements in the DOM. Under the hood, Halogen takes care of updating the actual DOM to match the code you have written.

Elements in Halogen accept two arguments:

  1. An array of attributes, properties, event handlers, and/or references to apply to the element. These correspond with ordinary HTML properties like placeholder and event handlers like onClick. We'll learn how to handle events in the next chapter, and we'll only focus on properties in this chapter.
  2. An array of children, if the element supports children.

As a brief example, let's translate this ordinary HTML into Halogen HTML:

<div id="root">
  <input placeholder="Name" />
  <button class="btn-primary" type="submit">
    Submit
  </button>
</div>

Let's break down our Halogen HTML:

  1. Our Halogen code has the same shape as our ordinary HTML: a div containing an input and a button, which itself contains plain text.
  2. Properties move from key-value pairs inside the tags into an array of properties for the element.
  3. Child elements move from being inside an open and closing tag into an array of children, if the element supports children.

Functions for writing properties in your HTML come from the Halogen.HTML.Properties module.

import Halogen.HTML as HH
import Halogen.HTML.Properties as HP

html =
  HH.div
    [ HP.id "root" ]
    [ HH.input
        [ HP.placeholder "Name" ]
    , HH.button
        [ HP.classes [ HH.ClassName "btn-primary" ]
        , HP.type_ HP.ButtonSubmit
        ]
        [ HH.text "Submit" ]
    ]

You can see Halogen's emphasis on type safety displayed here.

  1. A text input can't have children, so Halogen doesn't allow the element to take further elements as an argument.
  2. Only some values are possible for a button's type property, so Halogen restricts them with a sum type.
  3. CSS classes use a ClassName newtype so that they can be treated specially when needed; for example, the classes function ensures that your classes are space-separated when they're combined.

Some HTML elements and properties clash with reserved keywords in PureScript or with common functions from the Prelude, so Halogen adds an underscore to them. That's why you see type_ instead of type in the example above.

When you don't need to set any properties on a Halogen HTML element, you can use its underscored version instead. For example, the div and button elements below have no properties:

html = HH.div [ ] [ HH.button [ ] [ HH.text "Click me!"] ]

That means we can rewrite them using their underscored versions. This can help keep your HTML tidy.

html = HH.div_ [ HH.button_ [ HH.text "Click me!" ] ]

Writing Functions in Halogen HTML

It's common to write helper functions for Halogen HTML. Since Halogen HTML is built from ordinary PureScript functions, you can freely intersperse other functions in your code.

In this example, our function accepts an integer and renders it as text:

header :: forall w i. Int -> HH.HTML w i
header visits = 
  HH.h1_ 
    [ HH.text $ "You've had " <> show visits <> " visitors" ] 

We can also render lists of things:

lakes = [ "Lake Norman", "Lake Wylie" ]

html :: forall w i. HH.HTML w i
html = HH.div_ (map HH.text lakes)
-- same as: HH.div_ [ HH.text "Lake Norman", HH.text "Lake Wylie" ]

These function introduced a new type, HH.HTML, which you haven't seen before. Don't worry! This is the type of Halogen HTML, and we'll learn about it in the next section. For now, let's continue learning about using functions in HTML.

One common requirement is to conditionally render some HTML. You can do this with ordinary if and case statements, but it's useful to write helper functions for common patterns. Let's walk through two helper functions you might write in your own applications, which will help us get more practice writing functions with Halogen HTML.

First, you may sometimes need to deal with elements that may or may not exist. A function like the one below lets you render a value if it exists, and render an empty node otherwise.

maybeElem :: forall w i a. Maybe a -> (a -> HH.HTML w i) -> HH.HTML w i
maybeElem val f =
  case val of
    Just x -> f x
    _ -> HH.text ""

-- Render the name, if there is one
renderName :: forall w i. Maybe String -> HH.HTML w i
renderName mbName = maybeElem mbName \name -> HH.text name

Second, you may want to render some HTML only if a condition is true, without computing the HTML if it fails the condition. You can do this by hiding its evaluation behind a function so the HTML is only computed when the condition is true.

whenElem :: forall w i. Boolean -> (Unit -> HH.HTML w i) -> HH.HTML w i
whenElem cond f = if cond then f unit else HH.text ""

-- Render the old number, but only if it is different from the new number
renderOld :: forall w i. { old :: Number, new :: Number } -> HH.HTML w i
renderOld { old, new } = 
  whenElem (old /= new) \_ -> 
    HH.div_ [ HH.text $ show old ]

Now that we've explored a few ways to work with HTML, let's learn more about the types that describe it.

HTML Types

So far we've written HTML without type signatures. But when you write Halogen HTML in your application you'll include the type signatures.

HTML w i

HTML is the core type for HTML in Halogen. It is used for HTML elements that are not tied to a particular kind of component. For example, it's used as the type for the h1, text, and button elements we've seen so far. You can also use this type when defining your own custom HTML elements.

The HTML type takes two type parameters: w, which stands for "widget" and describes what components can be used in the HTML, and i, which stands for "input" and represents the type used to handle DOM events.

When you write helper functions for Halogen HTML that don't need to respond to DOM events, then you will typically use the HTML type without specifying what w and i are. For example, this helper function lets you create a button, given a label:

primaryButton :: forall w i. String -> HH.HTML w i
primaryButton label =
  HH.button
    [ HP.classes [ HH.ClassName "primary" ] ]
    [ HH.text label ]

You could also accept HTML as the label instead of accepting just a string:

primaryButton :: forall w i. HH.HTML w i -> HH.HTML w i
primaryButton label =
  HH.button
    [ HP.classes [ HH.ClassName "primary" ] ]
    [ label ]

Of course, being a button, you probably want to do something when it's clicked. Don't worry -- we'll cover handling DOM events in the next chapter!

ComponentHTML and PlainHTML

There are two other HTML types you will commonly see in Halogen applications.

ComponentHTML is used when you write HTML that is meant to work with a particular type of component. It can also be used outside of components, but it is most commonly used within them. We'll learn more about this type in the next chapter.

PlainHTML is a more restrictive version of HTML that's used for HTML that doesn't contain components and doesn't respond to events in the DOM. The type lets you hide HTML's two type parameters, which is convenient when you're passing HTML around as a value. However, if you want to combine values of this type with other HTML that does respond to DOM events or contain components, you'll need to convert it with fromPlainHTML.

IProp

When you look up functions from the Halogen.HTML.Properties and Halogen.HTML.Events modules, you'll see the IProp type featured prominently. For example, here's the placeholder function which will let you set the string placeholder property on a text field:

placeholder :: forall r i. String -> IProp (placeholder :: String | r) i
placeholder = prop (PropName "placeholder")

The IProp type is used for events and properties. It uses a row type to uniquely identify particular events and properties; when you then use one of these properties with a Halogen HTML element, Halogen is able to verify whether the element you're applying the property to actually supports it.

This is possible because Halogen HTML elements also carry a row type which lists all the properties and events that it can support. When you apply a property or event to the element, Halogen looks up in the HTML element's row type whether or not it supports the property or event.

This helps ensure your HTML is well-formed. For example, <div> elements do not support the placeholder property according to the DOM spec. Accordingly, if you try to give a div a placeholder property in Halogen you'll get a compile-time error:

-- ERROR: Could not match type ( placeholder :: String | r )
-- with type ( accessKey :: String, class :: String, ... )
html = HH.div [ HP.placeholder "blah" ] [ ]

This error tells you that you've tried to use a property with an element that doesn't support it. It first lists the property you tried to use, and then it lists the properties that the element does support. Another example of Halogen's type safety in action!

Adding missing properties

HTML is a living standard that is constantly being revised. Halogen tries to keep up with these changes, but sometimes falls behind. (If you have any ideas for how we can automate the process of detecting these changes, please let us know).

You'll likely discover that some properties are missing in Halogen. For example, you may try to write:

html = HH.iframe [ HP.sandbox "allow-scripts" ]

Only to receive this error:

Unknown value HP.sandbox

Even though it seems like this property should be supported:

type HTMLiframe = Noninteractive (height :: CSSPixel, name :: String, onLoad :: Event, sandbox :: String, src :: String, srcDoc :: String, width :: CSSPixel)

The solution is to write your own implementation of this missing property:

sandbox :: forall r i. String -> HH.IProp ( sandbox :: String | r ) i
sandbox = HH.prop (HH.PropName "sandbox")

Then you can use it in your HTML element:

html = HH.iframe [ sandbox "allow-scripts" ]

Please open an issue or PR to add this missing property. This is an easy way to contribute to Halogen.

Introducing Components

Halogen HTML is one basic building block of Halogen applications. But pure functions that produce HTML lack many essential features that a real world application needs: state that represents values over time, effects for things like network requests, and the ability to respond to DOM events (for example, when a user clicks a button).

Halogen components accept input and produce Halogen HTML, like the functions we've seen so far. Unlike functions, though, components maintain internal state, can update their state or perform effects in response to events, and can communicate with other components.

Halogen uses a component architecture. That means that Halogen uses components to let you split your UI into independent, reusable pieces and think about each piece in isolation. You can then combine components together to produce sophisticated applications.

For example, every Halogen application is made up of at least one component, which is called the "root" component. Halogen components can contain further components, and the resulting tree of components comprises your Halogen application.

In this chapter we'll learn most of the essential types and functions for writing Halogen components. For a beginner, this is the hardest chapter in the guide because many of these concepts will be brand-new. Don't worry if it feels overwhelming the first time you read it! You'll use these types and functions over and over again when you write Halogen applications, and they soon become second nature. If you're having a hard time with the chapter, try reading it again while building a simple component other than the one described here.

In this chapter we'll also see more examples of Halogen's declarative style of programming. When you write a component you're responsible for describing what UI should exist for any given internal state. Halogen, under the hood, updates the actual DOM elements to match your desired UI.

A Tiny Example

We have already seen a simple example of a component: a counter that can be incremented or decremented.

module Main where

import Prelude

import Halogen as H
import Halogen.HTML as HH
import Halogen.HTML.Events as HE

data Action = Increment | Decrement

component =
  H.mkComponent
    { initialState
    , render
    , eval: H.mkEval H.defaultEval { handleAction = handleAction }
    }
  where
  initialState _ = 0

  render state =
    HH.div_
      [ HH.button [ HE.onClick \_ -> Decrement ] [ HH.text "-" ]
      , HH.text (show state)
      , HH.button [ HE.onClick \_ -> Increment ] [ HH.text "+" ]
      ]

  handleAction = case _ of
    Decrement ->
      H.modify_ \state -> state - 1

    Increment ->
      H.modify_ \state -> state + 1

This component maintains an integer as its internal state, and updates that state in response to click events on the two buttons.

This component works, but in a real world application we wouldn't leave all the types unspecified. Let's rebuild this component from scratch with all the types it uses.

Building a Basic Component (With Types)

A typical Halogen component accepts input, maintains an internal state, produces Halogen HTML from that state, and updates its state or performs effects in response to events. In this case we don't need to perform any effects, but we'll cover them soon.

Let's break down each part of our component, assigning types along the way.

Input

Halogen components can accept input from a parent component or the root of the application. If you think of a component as a function, then input is the function's argument.

If your component takes input, then you should describe it with a type. For example, a component that accepts an integer as input would use this type:

type Input = Int

Our counter doesn't require any input, so we have two choices. First, we can just say that our input type is Unit, meaning that we'll just take a dummy value and throw it away:

type Input = Unit

Second, and more commonly, anywhere our input type shows up in our component we can simply leave it as a type variable: forall i. .... It's perfectly fine to use either approach, but from here on out we'll use type variables to represent types our component isn't using.

State

Halogen components maintain an internal state over time, which is used to drive the component's behavior and to produce HTML. Our counter component maintains the current count, an integer, so we'll use that as our state type:

type State = Int

Our component needs to also produce an initial state value. All Halogen components require an initialState function which produces the initial state from the input value:

initialState :: Input -> State

Our counter component doesn't use its input, so our initialState function won't use an input type and will instead just leave that type variable open. Our counter should start at 0 when the component runs.

initialState :: forall input. input -> State
initialState _ = 0

Actions

Halogen components can update state, perform effects, and communicate with other components in response to events that arise internally. Components use an "action" type to describe what kinds of things a component can do in response to internal events.

Our counter has two internal events:

  1. a click event on the "-" button to decrement the count
  2. a click event on the "+" button to increment the count.

We can describe what our component should do in response to these events using a data type we'll call Action:

data Action = Increment | Decrement

This type signifies that our component is capable of incrementing and decrementing. In a moment, we'll see this type used in our HTML -- another example of Halogen's declarative nature.

Just like how our state type had to be paired with an initialState function that describes how to produce a State value, our Action type should be paired with a function called handleAction that describes what to do when one of these actions occurs.

handleAction :: forall output m. Action -> H.HalogenM State Action () output m Unit

As with our input type, we can leave type variables open for types that we aren't using.

  • The type () means our component has no child components. We could also leave it open as a type variable because we aren't using it -- slots, by convention -- but () is so short you'll see this type commonly used instead.
  • The output type parameter is only used when your component communicates with a parent.
  • The m type parameter is only relevant when your component performs effects.

Since our counter has no child components we'll use () to describe them, and because it doesn't communicate with a parent or perform effects we'll leave the output and m type variables open.

Here's the handleAction function for our counter:

handleAction :: forall output m. Action -> H.HalogenM State Action () output m Unit
handleAction = case _ of
  Decrement ->
    H.modify_ \state -> state - 1

  Increment ->
    H.modify_ \state -> state + 1

Our handleAction function responds to Decrement by reducing our state variable by 1, and to Increment by increasing our state variable by 1. Halogen provides several update functions you can use in your handleAction function; these ones are commonly used:

  • modify allows you to update the state, given the previous state, returning the new state
  • modify_ is the same as modify, but it doesn't return the new state (thus you don't have to explicitly discard the result, as you would with modify)
  • get allows you to retrieve the current state
  • gets allows you to retrieve the current state and also apply a function to it (most commonly, _.fieldName to retrieve a particular field from a record)

We'll talk more about HalogenM when we talk about performing effects. Our counter doesn't perform effects, so all we need are the state update functions.

Rendering

Halogen components produce HTML from their state using a function called render. The render function runs every time the state changes. This is what makes Halogen declarative: for any given state, you describe the UI that it corresponds to. Halogen handles the workload of ensuring that state changes always result in the UI you described.

Render functions in Halogen are pure, which means that you can't do things like get the current time, make network requests, or anything like that during rendering. All you can do is produce HTML for your state value.

When we look at the type of our render function we can see the ComponentHTML type we touched on last chapter. This type is a more specialized version of the HTML type, meant specifically for HTML produced in components. Once again, we'll use () and leave m open because they are only relevant when using child components, which we'll cover in a later chapter.

render :: forall m. State -> H.ComponentHTML Action () m

Now that we're working with our render function, we're back to the Halogen HTML that should be familiar from the last chapter! You can write regular HTML in ComponentHTML just like we did last chapter:

import Halogen.HTML.Events

render :: forall m. State -> H.ComponentHTML Action () m
render state =
  HH.div_
    [ HH.button [ HE.onClick \_ -> Decrement ] [ HH.text "-" ]
    , HH.text (show state)
    , HH.button [ HE.onClick \_ -> Increment ] [ HH.text "+" ]
    ]

Handling Events

We can now see how to handle events in Halogen. First, you write the event handler in the properties array along with any other properties, attributes, and refs you might need. Then, you associate the event handler with an Action that your component knows how to handle. Finally, when the event occurs, your handleAction function is called to handle the event.

You might be curious about why we provided an anonymous function to onClick. To see why, we can look at the actual type of onClick:

onClick
  :: forall row action
   . (MouseEvent -> action)
  -> IProp (onClick :: MouseEvent | row) action

-- Specialized to our component
onClick
  :: forall row
   . (MouseEvent -> Action)
  -> IProp (onClick :: MouseEvent | row) Action

In Halogen, event handlers take as their first argument a callback. This callback receives the DOM event that occurred (in the case of a click event, that's a MouseEvent), which contains some metadata you may want to use, and is then responsible for returning an action that Halogen should run in response to the event. In our case, we won't inspect the event itself, so we throw the argument away and return the action we want to run (Increment or Decrement).

The onClick function then returns a value of type IProp. You should remember IProp from the previous chapter. As a refresher, Halogen HTML elements specify a list of what properties and events they support. Properties and events in turn specify their type. Halogen is then able to ensure that you never use a property or event on an element that doesn't support it. In this case buttons do support onClick events, so we're good to go!

In this simple example, the MouseEvent parameter is ignored by the handler function passed to onClick, since the action is completely determined by which button receives the click. We will talk about accessing the event itself after we have looked at effects in section 3 of this guide.

Bringing It All Together

Let's bring each of our types and functions back together to produce our counter component -- this time with types specified. Let's revisit the types and functions that we wrote:

-- This can be specified if your component takes input, or you can leave
-- the type variable open if your component doesn't.
type Input = Unit

type State = Int

initialState :: forall input. input -> State
initialState = ...

data Action = Increment | Decrement

handleAction :: forall output m. Action -> H.HalogenM State Action () output m Unit
handleAction = ...

render :: forall m. State -> H.ComponentHTML Action () m
render = ...

These types and functions are the core building blocks of a typical Halogen component. But they aren't sufficient on their own like this -- we need to bring them all together in one place.

We'll do that using the H.mkComponent function. This function takes a ComponentSpec, which is a record containing an initialState, render, and eval function, and produces a Component from it:

component =
  H.mkComponent
    { -- First, we provide our function that describes how to produce the first state
      initialState
      -- Then, we provide our function that describes how to produce HTML from the state
    , render
      -- Finally, we provide our function that describes how to handle actions that
      -- occur while the component is running, which updates the state.
    , eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
    }

We'll talk more about the eval function in future chapters. For the time being you can think of the eval function as defining how the component responds to events; for now, the only kind of events we care about are actions, and so the only function we'll use is handleAction.

Our component is now complete, but we're missing one last type definition: our component type.

The H.Component Type

The mkComponent function produces a component from a ComponentSpec, which is a record of the functions that Halogen needs to run a component. We'll get into more detail about this type in a subsequent chapter.

mkComponent :: H.ComponentSpec ... -> H.Component query input output m

The resulting component has the type H.Component, which itself takes four type parameters that describe the public interface of the component. Our component doesn't communicate with parent components or child components, so it doesn't use any of these type variables. Still, we'll briefly step through them now so you know what's coming in subsequent chapters.

  1. The first parameter query represents a way that parent components can communicate with this component. We will talk about it more when we talk about parent and child components.
  2. The second parameter input represents the input our component accepts. In our case, the component doesn't accept any input, so we'll leave this variable open.
  3. The third parameter output represents a way that this component can communicate with its parent component. We'll talk about it more when we talk about parent and child components.
  4. The final parameter, m, represents the monad that can be used to run effects in the component. Our component doesn't run any effects, so we'll leave this variable open.

Our counter component can therefore be specified by leaving all of the H.Component type variables open.

The Final Product

That was a lot to take in! We've finally got our counter component fully specified with types. If you can comfortably build components like this one, you're most of the way to a thorough understanding of building Halogen components in general. The rest of this guide will build on top of your understanding of state, actions, and rendering HTML.

We've added a main function that runs our Halogen application so that you can try this example out by pasting it into Try PureScript. We'll cover how to run Halogen applications in a later chapter -- for now you can ignore the main function and focus on the component we've defined.

module Main where

import Prelude

import Effect (Effect)
import Halogen as H
import Halogen.Aff as HA
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.VDom.Driver (runUI)

main :: Effect Unit
main = HA.runHalogenAff do
  body <- HA.awaitBody
  runUI component unit body

type State = Int

data Action = Increment | Decrement

component :: forall query input output 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 _ = 0

render :: forall m. State -> H.ComponentHTML Action () m
render state =
  HH.div_
    [ HH.button [ HE.onClick \_ -> Decrement ] [ HH.text "-" ]
    , HH.text (show state)
    , HH.button [ HE.onClick \_ -> Increment ] [ HH.text "+" ]
    ]

handleAction :: forall output m. Action -> H.HalogenM State Action () output m Unit
handleAction = case _ of
  Decrement ->
    H.modify_ \state -> state - 1

  Increment ->
    H.modify_ \state -> state + 1

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:

  1. We added a MonadEffect constraint to the m type parameter for our component and for our handleAction function. We don't need the constraint for our render function because we don't have any child components.
  2. We actually used an effect for the first time: the random function, which comes from Effect.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]
  1. We have constrained our m type parameter to say we support any monad, so long as that monad supports MonadEffect. It's another way to say "We need to be able to use Effect functions in our evaluation code."
  2. The random function has the type Effect Number. But we can't use it directly: our component doesn't support Effect but rather any monad m so long as that monad can run effects from Effect. It's a subtle difference, but in the end we require the random function to have the type MonadEffect m => m Number instead of being Effect directly. Fortunately, we can convert any Effect type to MonadEffect m => m using the liftEffect function. This is a common pattern in Halogen, so keep liftEffect in mind if you're using MonadEffect.
  3. The modify_ function lets you update state, and it comes directly from HalogenM 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:

  1. It mixes together functions from multiple monads (preventDefault is Effect, AX.get is Aff, and gets and modify_ are HalogenM). We're able to use liftEffect and liftAff along with our constraints to make sure everything plays well together.
  2. We only have one constraint, MonadAff. That's because anything that can be run in Effect can also be run in Aff, so MonadAff implies MonadEffect.
  3. 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:

  1. Set loading to true, which causes the component to re-render and display "Working..."
  2. Set loading to false 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 (the SetUsername action).
  • preventDefault is called on the event in the onSubmit handler (the MakeRequest 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.

Lifecycles and Subscriptions

The concepts you've learned so far cover the majority of Halogen components you'll write. Most components have internal state, render HTML elements, and respond by performing actions when users click, hover over, or otherwise interact with the rendered HTML.

But actions can arise internally from other kinds of events, too. Here are some common examples:

  1. You need to run an action when the component starts up (for example, you need to perform an effect to get your initial state) or when the component is removed from the DOM (for example, to clean up resources you acquired). These are called lifecycle events.
  2. You need to run an action at regular intervals (for example, you need to perform an update every 10 seconds), or when an event arises from outside your rendered HTML (for example, you need to run an action when a key is pressed on the DOM window, or you need to handle events that occur in a third-party component like a text editor). These are handled by subscriptions.

We'll learn about one other way actions can arise in a component when we learn about parent and child components in the next chapter. This chapter will focus on lifecycles and subscriptions.

Lifecycle Events

Every Halogen component has access to two lifecycle events:

  1. The component can evaluate an action when it is initialized (Halogen creates it)
  2. The component can evaluate an action when it is finalized (Halogen removes it)

We specify what action (if any) to run when the component is initialized and finalized as part of the eval function -- the same place where we've been providing the handleAction function. In the next section we'll get into more detail about what eval is, but first lets see an example of lifecycles in action.

The following example is nearly identical to our random number component, but with some important changes.

  1. We have added Initialize and Finalize in addition to our existing Regenerate action.
  2. We've expanded our eval to include an initialize field that states our Initialize action should be evaluated when the component initializes, and a finalize field that states our Finalize action should be evaluated when the component finalizes.
  3. Since we have two new actions, we've added two new cases to our handleAction function to describe how to handle them.

Try reading through the example:

module Main where

import Prelude

import Data.Maybe (Maybe(..), maybe)
import Effect (Effect)
import Effect.Class (class MonadEffect)
import Effect.Class.Console (log)
import Effect.Random (random)
import Halogen as H
import Halogen.Aff as HA
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.VDom.Driver (runUI)

main :: Effect Unit
main = HA.runHalogenAff do
  body <- HA.awaitBody
  runUI component unit body

type State = Maybe Number

data Action
  = Initialize
  | Regenerate
  | Finalize

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
        , initialize = Just Initialize
        , finalize = Just Finalize
        }
    }

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
  Initialize -> do
    handleAction Regenerate
    newNumber <- H.get
    log ("Initialized: " <> show newNumber)

  Regenerate -> do
    newNumber <- H.liftEffect random
    H.put (Just newNumber)

  Finalize -> do
    number <- H.get
    log ("Finalized! Last number was: " <> show number)

When this component mounts we'll generate a random number and log it to the console. We'll keep regenerating random numbers as the user clicks the button, and when this component is removed from the DOM it will log the last number it had in state.

We made one other interesting change in this example: in our Initialize handler we called handleAction Regenerate -- we called handleAction recursively. It can be convenient to call actions from within other actions from time to time as we've done here. We could have also inlined Regenerate's handler -- the following code does the same thing:

  Initialize -> do
    newNumber <- H.liftEffect random
    H.put (Just newNumber)
    log ("Initialized: " <> show newNumber)

Before we move on to subscriptions, let's talk more about the eval function.

The eval Function, mkEval, and EvalSpec

We've been using eval in all of our components, but so far we've only handled actions arising from our Halogen HTML via the handleAction function. But the eval function can describe all the ways our component can evaluate HalogenM code in response to events.

In the vast majority of cases you don't need to care much about all the types and functions involved in the component spec and eval spec described below, but we'll briefly break down the types so you have an idea of what's going on.

The mkComponent function takes a ComponentSpec, which is a record containing three fields:

H.mkComponent
  { initialState :: input -> state
  , render :: state -> H.ComponentHTML action slots m
  , eval :: H.HalogenQ query action input ~> H.HalogenM state action slots output m
  }

We've spent plenty of time with the initialState and render functions already. But the eval function may look strange -- what is HalogenQ, and how do functions like handleAction fit in? For now, we'll focus on the most common use of this function, but you can find the full details in the Concepts Reference.

The eval function describes how to handle events that arise in the component. It's usually constructed by applying the mkEval function to an EvalSpec, the same way we applied mkComponent to a ComponentSpec to produce a Component.

For convenience, Halogen provides an already-complete EvalSpec called defaultEval, which does nothing when an event arises in the component. By using this default value you can override just the values you care about, while leaving the rest of them doing nothing.

Here's how we've defined eval functions that only handle actions so far:

H.mkComponent
  { initialState
  , render
  , eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
  }

-- assuming we've defined a `handleAction` function in scope...
handleAction = ...

Note: initialState and render are set using abbreviated record pun notation; however, handleAction cannot be set with a pun in this case because it is part of a record update. More information about record pun and record update syntax is available in the Records Language Reference.

You can override more fields, if you need to. For example, if you need to support an initializer then you would override the initialize field too:

H.mkComponent
  { initialState
  , render
  , eval: H.mkEval $ H.defaultEval
      { handleAction = handleAction
      , initialize = Just Initialize
      }
  }

Let's take a quick look at the full type of EvalSpec:

type EvalSpec state query action slots input output m =
  { handleAction :: action -> HalogenM state action slots output m Unit
  , handleQuery :: forall a. query a -> HalogenM state action slots output m (Maybe a)
  , initialize :: Maybe action
  , receive :: input -> Maybe action
  , finalize :: Maybe action
  }

The EvalSpec covers all the types available internally in your component. Fortunately, you don't need to specify this type anywhere -- you can just provide a record to mkEval. We'll cover the handleQuery and receive functions as well as the query and output types in the next chapter, as they're only relevant for child components.

Since in normal use you'll override specific fields from defaultEval rather than write out a whole eval spec yourself, let's also look at what defaultEval implements for each of these functions:

defaultEval =
  { handleAction: const (pure unit)
  , handleQuery: const (pure Nothing) -- we'll learn about this when we cover child components
  , initialize: Nothing
  , receive: const Nothing -- we'll learn about this when we cover child components
  , finalize: Nothing
  }

Now, let's move to the other common source of internal events: subscriptions.

Subscriptions

Sometimes you need to handle events arising internally that don't come from a user interacting with the Halogen HTML you've rendered. Two common sources are time-based actions and events that happen on an element outside one you've rendered (like the browser window).

In Halogen these kinds of events can be created manually with the halogen-subscriptions library. Halogen components can subscribe to an Emitter by providing an action that should run when the emitter fires.

You can subscribe to events using functions from the halogen-subscriptions library, but Halogen provides a special helper function for subscribing to event listeners in the DOM called eventListener.

An Emitter produces a stream of actions, and your component will evaluate those actions so long as it remains subscribed to the emitter. It's common to create an emitter and subscribe to it when the component initializes, though you can subscribe or unsubscribe from an emitter at any time.

Let's see two examples of subscriptions in action: an Aff-based timer that counts the seconds since the component mounted and an event-listener-based stream that reports keyboard events on the document.

Implementing a Timer

Our first example will use an Aff-based timer to increment every second.

module Main where

import Prelude

import Control.Monad.Rec.Class (forever)
import Data.Maybe (Maybe(..))
import Effect (Effect)
import Effect.Aff (Milliseconds(..))
import Effect.Aff as Aff
import Effect.Aff.Class (class MonadAff)
import Effect.Exception (error)
import Halogen as H
import Halogen.Aff as HA
import Halogen.HTML as HH
import Halogen.Subscription as HS
import Halogen.VDom.Driver (runUI)

main :: Effect Unit
main = HA.runHalogenAff do
  body <- HA.awaitBody
  runUI component unit body

data Action = Initialize | Tick

type State = Int

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
        , initialize = Just Initialize
        }
    }

initialState :: forall input. input -> State
initialState _ = 0

render :: forall m. State -> H.ComponentHTML Action () m
render seconds = HH.text ("You have been here for " <> show seconds <> " seconds")

handleAction :: forall output m. MonadAff m => Action -> H.HalogenM State Action () output m Unit
handleAction = case _ of
  Initialize -> do
    _ <- H.subscribe =<< timer Tick
    pure unit

  Tick ->
    H.modify_ \state -> state + 1

timer :: forall m a. MonadAff m => a -> m (HS.Emitter a)
timer val = do
  { emitter, listener } <- H.liftEffect HS.create
  _ <- H.liftAff $ Aff.forkAff $ forever do
    Aff.delay $ Milliseconds 1000.0
    H.liftEffect $ HS.notify listener val
  pure emitter

Almost all of this code should look familiar, but there are two new parts.

First, we've defined a reusable Emitter that will broadcast a value of our choice every second until it has no subscribers:

timer :: forall m a. MonadAff m => a -> m (HS.Emitter a)
timer val = do
  { emitter, listener } <- H.liftEffect HS.create
  _ <- H.liftAff $ Aff.forkAff $ forever do
    Aff.delay $ Milliseconds 1000.0
    H.liftEffect $ HS.notify listener val
  pure emitter

Unless you are creating emitters tied to event listeners in the DOM, you should use functions from the halogen-subscriptions library. Most commonly you'll use HS.create to create an emitter and a listener, but if you need to manually control unsubscription you can also use HS.makeEmitter.

Second, we use the subscribe function from Halogen to attach to the emitter, also providing the specific action we'd like to emit every second:

  Initialize -> do
    _ <- H.subscribe =<< timer Tick
    pure unit

The subscribe function takes an Emitter as an argument and it returns a SubscriptionId. You can pass this SubscriptionId to the Halogen unsubscribe function at any point to end the subscription. Components automatically end any subscriptions it has when they finalize, so there's no requirement to unsubscribe here.

You may also be interested in the Ace editor example, which subscribes to events that happen inside a third-party JavaScript component and uses them to trigger actions in a Halogen component.

Using Event Listeners As Subscriptions

Another common reason to use subscriptions is when you need to react to events in the DOM that don't arise directly from HTML elements you control. For example, we might want to listen to events that happen on the document itself.

In the following example we subscribe to key events on the document, save any characters that are typed while holding the Shift key, and stop listening if the user hits the Enter key. It demonstrates using the eventListener function to attach an event listener and using the H.unsubscribe function to choose when to clean it up.

There is also a corresponding example of keyboard input in the examples directory.

module Main where

import Prelude

import Data.Maybe (Maybe(..))
import Data.String as String
import Effect (Effect)
import Effect.Aff.Class (class MonadAff)
import Halogen as H
import Halogen.Aff as HA
import Halogen.HTML as HH
import Halogen.Query.Event (eventListener)
import Halogen.VDom.Driver (runUI)
import Web.Event.Event as E
import Web.HTML (window)
import Web.HTML.HTMLDocument as HTMLDocument
import Web.HTML.Window (document)
import Web.UIEvent.KeyboardEvent as KE
import Web.UIEvent.KeyboardEvent.EventTypes as KET

main :: Effect Unit
main = HA.runHalogenAff do
  body <- HA.awaitBody
  runUI component unit body

type State = { chars :: String }

data Action
  = Initialize
  | HandleKey H.SubscriptionId KE.KeyboardEvent

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
        , initialize = Just Initialize
        }
    }

initialState :: forall input. input -> State
initialState _ = { chars: "" }

render :: forall m. State -> H.ComponentHTML Action () m
render state =
  HH.div_
    [ HH.p_ [ HH.text "Hold down the shift key and type some characters!" ]
    , HH.p_ [ HH.text "Press ENTER or RETURN to clear and remove the event listener." ]
    , HH.p_ [ HH.text state.chars ]
    ]

handleAction :: forall output m. MonadAff m => Action -> H.HalogenM State Action () output m Unit
handleAction = case _ of
  Initialize -> do
    document <- H.liftEffect $ document =<< window
    H.subscribe' \sid ->
      eventListener
        KET.keyup
        (HTMLDocument.toEventTarget document)
        (map (HandleKey sid) <<< KE.fromEvent)

  HandleKey sid ev
    | KE.shiftKey ev -> do
        H.liftEffect $ E.preventDefault $ KE.toEvent ev
        let char = KE.key ev
        when (String.length char == 1) do
          H.modify_ \st -> st { chars = st.chars <> char }

    | KE.key ev == "Enter" -> do
        H.liftEffect $ E.preventDefault (KE.toEvent ev)
        H.modify_ _ { chars = "" }
        H.unsubscribe sid

    | otherwise ->
        pure unit

In this example we used the H.subscribe' function, which passes the SubscriptionId to the emitter instead of returning it. This is an alternative that lets you keep the ID in the action type instead of the state, which can be more convenient.

We wrote our emitter right into our code to handle the Initialize action, which registers an event listener on the document and emits HandleKey every time a key is pressed.

eventListener uses types from the purescript-web libraries for working with the DOM to manually construct an event listener:

eventListener
  :: forall a
   . Web.Event.EventType
  -> Web.Event.EventTarget.EventTarget
  -> (Web.Event.Event -> Maybe a)
  -> HS.Emitter a

It takes a type of event to listen to (in our case: keyup), a target indicating where to listen for events (in our case: the HTMLDocument itself), and a callback function that transforms the events that occur into a type that should be emitted (in our case: we emit our Action type by capturing the event in the HandleKey constructor).

Wrapping Up

Halogen components use the Action type to handle various kinds of events that arise internally in a component. We've now seen all the common ways this can happen:

  1. User interaction with HTML elements we rendered
  2. Lifecycle events
  3. Subscriptions, whether via Aff and Effect functions or from event listeners on the DOM

You now know all the essentials for using Halogen components in isolation. In the next chapter we'll learn how to combine Halogen components together into a tree of parent and child components.

Parent and Child Components

Halogen is an unopinionated UI library: it allows you to create declarative user interfaces without enforcing a particular architecture.

Our applications so far have consisted of a single Halogen component. You can build large applications as a single component and break the state and the handleAction and render functions into separate modules as the app grows. This lets you use the Elm architecture in Halogen.

However, Halogen supports architectures with arbitrarily deep trees of components. That means any component you write is allowed to contain more components, each with their own state and behaviors. Most Halogen applications use a component architecture in this way, including the Real World Halogen app.

When you move from a single component to many components you begin to need mechanisms so that components can communicate with one another. Halogen gives us three ways for a parent and child component to communicate:

  1. A parent component can send queries to a child component, which either tell the child component to do something or request some information from it.
  2. A parent component gives a child component the input it needs, which is re-sent every time the parent component renders.
  3. A child component can emit output messages to the parent component, notifying it when an important event has occurred.

These type parameters are represented in the Component type, and some are also found in the ComponentHTML and HalogenM types. For example, a component that supports queries, input, and output messages will have this Component type:

component :: forall m. H.Component Query Input Output m

You can think of the ways a component can communicate with other components as its public interface, and the public interface shows up in the Component type.

In this chapter we'll learn about:

  1. How to render components in your Halogen HTML
  2. The three ways that components communicate: queries, input, and output messages
  3. Component slots, the slot function, and the Slot type, which make this communication type-safe

We'll start by rendering a simple child component that has no queries or output messages. Then, we'll build up components that use these ways to communicate, ending with a final example that shows off a parent and child component using all of these mechanisms at once.

Try loading the example into Try PureScript to explore each of the communication mechanisms discussed in this chapter!

Rendering Components

We began this guide by writing functions that returned Halogen HTML elements. These functions could be used by other functions to build even larger trees of HTML elements.

When we started using components we began writing render functions. Conceptually, components produce Halogen HTML as their result via this function, though they can also maintain internal state and perform effects, among other things.

In fact, while we've only been using HTML elements when writing our render functions so far, we can also use components as if they were functions that produce HTML. The analogy is imperfect, but it can be a helpful mental model for understanding how to treat components when you are writing your render function.

When one component renders another, it's called the "parent" component and the component it renders is called the "child" component.

Let's see how we can render a component inside our render function, instead of only HTML elements as we've seen so far. We'll start by writing a component that uses a helper function to render a button. Then, we'll turn that helper function into its own component, and we'll adjust the parent component to render this new child component.

First, we'll write a component that uses a helper function to render some HTML:

module Main where

import Prelude

import Halogen as H
import Halogen.HTML as HH

parent :: forall query input output m. H.Component query input output m
parent =
  H.mkComponent
    { initialState: identity
    , render
    , eval: H.mkEval H.defaultEval
    }
  where
  render :: forall state action. state -> H.ComponentHTML action () m
  render _ = HH.div_ [ button { label: "Click Me" } ]

button :: forall w i. { label :: String } -> HH.HTML w i
button { label } = HH.button [ ] [ HH.text label ]

This should look familiar. We have a simple component that renders a div, and a helper function, button, which renders a button given a label as input. As a note, our parent component leaves type variables open for our state and actions because it doesn't have an internal state and it doesn't have any actions.

Now, let's turn our button function into a component for demonstration purposes (in a real world app it would be too small for that):

type Input = { label :: String }

type State = { label :: String }

button :: forall query output m. H.Component query Input output m
button =
  H.mkComponent
    { initialState
    , render
    , eval: H.mkEval H.defaultEval
    }
  where
  initialState :: Input -> State
  initialState input = input

  render :: forall action. State -> H.ComponentHTML action () m
  render { label } = HH.button [ ] [ HH.text label ]

We took a few steps to convert our button HTML function into a button component:

  1. We converted the argument to our helper function into the Input type for the component. The parent component is responsible for providing this input to our component. We'll learn more about input in the next section.
  2. We moved our HTML into the component's render function. The render function only has access to our component's State type, so in our initialState function we copied our input value into our state so we could render it. Copying input into state is a common pattern in Halogen. Also notice that our render function leaves the action type unspecified (because we don't have any actions) and indicates we have no child components using ().
  3. We used defaultEval, unmodified, as our EvalSpec because this component doesn't need to respond to events arising internally -- it has no actions and uses no lifecycle events, for example.

Our parent component is now broken, though! If you've been following along, you'll now see an error:

[1/1 TypesDoNotUnify]

  16    render _ = HH.div_ [ button { label: "Click Me" } ]
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  Could not match type

    Component HTML t2 { label :: String }

  with type

    Function

Components can't just be rendered by giving the component its input as a function argument. Even though components produce ordinary Halogen HTML they can also communicate with the parent component; for this reason, components need extra information before they can be rendered like an ordinary element.

Conceptually, components occupy a "slot" in your tree of HTML. This slot is a place where the component can produce Halogen HTML until it is removed from the DOM. A component in a slot can be thought of as a dynamic, stateful HTML element. You can freely intermix these dynamic elements with ordinary Halogen HTML elements, but the dynamic elements need more information.

That extra information comes from the slot function and the slot type used in ComponentHTML, which we've so far been leaving as the empty row, (). We'll talk a lot more about rendering components in slots in a moment, but for now let's get things compiling.

We can fix our render function by rendering our component in a slot via the slot function. We'll also update the slot type in our ComponentHTML to include the component our Halogen HTML now must support. This diff demonstrates the differences between rendering an HTML element and rendering a component:

+ import Type.Proxy (Proxy(..))
+
+ type Slots = ( button :: forall query. H.Slot query Void Int )
+
+ _button = Proxy :: Proxy "button"

  parent :: forall query input output m. H.Component query input output m
  parent =
    H.mkComponent
      { initialState: identity
      , render
      , eval: H.mkEval H.defaultEval
      }
    where
-   render :: forall state action. state -> H.ComponentHTML action () m
+   render :: forall state action. state -> H.ComponentHTML action Slots m
    render _ =
-     HH.div_ [ button { label: "Click Me" } ]
+     HH.div_ [ HH.slot_ _button 0 button { label: "Click Me" } ]

Our parent component is now rendering a child component -- our button component. Rendering a component introduced two big changes:

  1. We used the slot_ function to render the component, which takes several arguments we haven't explored yet. Two of those arguments are the button component itself and the label it needs as input.
  2. We added a new type called Slots, which is a row containing a label for our button component with a value of type H.Slot, and we used this new type in our ComponentHTML instead of the previous empty row () we've seen so far.

The slot and slot_ functions and the Slot type let you render a stateful, effectful child component in your Halogen HTML as if it were any other HTML element. But why are there so many arguments and types involved in doing this? Why can't we just call button with its input?

The answer is that Halogen provides two ways for a parent and child component to communicate with one another, and we need to ensure that this communication is type-safe. The slot function allows us to:

  1. Decide how to identify a particular component by a label (the type-level string "button", which we represent at the term level with the proxy Proxy :: Proxy "button") and a unique identifier (the integer 0, in this case) so that we can send it queries. This is an imperative form of communication from the parent to the child.
  2. Render the component (button) and give it its input ({ label: "Click Me" }), which will be re-sent every time the parent component renders in case the input changes over time. This is a declarative form of communication from the parent to the child.
  3. Decide how to handle output messages from the child component. The slot function lets you provide a handler for child outputs, while the slot_ function can be used when a child component doesn't have any outputs or you want to ignore them. This is communication from the child to the parent.

The slot and slot_ functions and the H.Slot type let us manage these three communication mechanisms in a type-safe way. In the rest of this chapter we'll focus on how parent and child components communicate with one another, and along the way we'll explore slots and slot types.

Communicating Among Components

When you move from using one component to using many components you'll soon need some way for them to communicate with one another. In Halogen there are three ways that a parent and child component can communicate directly:

  1. The parent component can provide input to the child component. Each time the parent component renders it will send the input again, and then it's up to the child component to decide what to do with the new input.
  2. The child component can emit output messages to the parent, similar to how we've been using subscriptions so far. The child component can notify the parent component when an important event has happened, like a modal closing or a form being submitted, and then the parent can decide what to do.
  3. The parent component can query the child component, either by telling it to do something or by requesting some information from it. The parent component can decide when it needs the child component to do something or give it some information, and then it's up to the child component to handle the query.

These three mechanisms give you several ways to communicate between components. Let's briefly explore these three mechanisms, and then we'll see how the slot function and the slot type you define for your component help you use them in a type-safe way.

Input

Parent components can provide input to child components, which is sent on every render. We've seen this several times already -- the input type is used to produce the child component's initial state. In the example which introduced this chapter our button component received its label from the parent component.

So far we've only used input to produce our initial state. But input doesn't stop once the initial state has been created. The input is sent again on every render, and the child component can handle the new input via the receive function in its eval spec.

receive :: input -> Maybe action

The receive function in the eval spec should remind you of initialize and finalize, which let you choose an action to evaluate when the component is created and destroyed. In the same way, the receive function lets you choose an action to evaluate when the parent component sends new input.

By default Halogen's defaultSpec doesn't provide an action to be evaluated when new input is received. If your child component doesn't need to do anything after it receives its initial value then you can leave this as-is. For example, once our button received its label and copied it into state there was no need to continue listening to the input in case it changed over time.

The ability to receive new input every time the parent renders is a powerful feature. It means parent components can declaratively provide values to child components. There are other ways for a parent component to communicate with a child component, but the declarative nature of input makes it the best choice in most circumstances.

Let's make this concrete by revisiting our example from the introduction. In this version our button is unchanged -- it receives its label as input and uses it to set its initial state -- but our parent component has changed. Our parent component now starts a timer when it initializes, increments a count every second, and uses the count in state as the label for the button.

In short, our button's input will be re-sent every second. Try pasting this into Try PureScript to see what happens -- does our button's label update every second?

module Main where

import Prelude

import Control.Monad.Rec.Class (forever)
import Data.Maybe (Maybe(..))
import Effect (Effect)
import Effect.Aff (Milliseconds(..))
import Effect.Aff as Aff
import Effect.Aff.Class (class MonadAff)
import Halogen as H
import Halogen.Aff (awaitBody, runHalogenAff)
import Halogen.HTML as HH
import Halogen.Subscription as HS
import Halogen.VDom.Driver (runUI)
import Type.Proxy (Proxy(..))

main :: Effect Unit
main = runHalogenAff do
  body <- awaitBody
  runUI parent unit body

type Slots = ( button :: forall q. H.Slot q Void Unit )

_button = Proxy :: Proxy "button"

type ParentState = { count :: Int }

data ParentAction = Initialize | Increment

parent :: forall query input output m. MonadAff m => H.Component query input output m
parent =
  H.mkComponent
    { initialState
    , render
    , eval: H.mkEval $ H.defaultEval
        { handleAction = handleAction
        , initialize = Just Initialize
        }
    }
  where
  initialState :: input -> ParentState
  initialState _ = { count: 0 }

  render :: ParentState -> H.ComponentHTML ParentAction Slots m
  render { count } =
    HH.div_ [ HH.slot_ _button unit button { label: show count } ]

  handleAction :: ParentAction -> H.HalogenM ParentState ParentAction Slots output m Unit
  handleAction = case _ of
    Initialize -> do
      { emitter, listener } <- H.liftEffect HS.create
      void $ H.subscribe emitter
      void
        $ H.liftAff
        $ Aff.forkAff
        $ forever do
            Aff.delay $ Milliseconds 1000.0
            H.liftEffect $ HS.notify listener Increment
    Increment -> H.modify_ \st -> st { count = st.count + 1 }

-- Now we turn to our child component, the button.

type ButtonInput = { label :: String }

type ButtonState = { label :: String }

button :: forall query output m. H.Component query ButtonInput output m
button =
  H.mkComponent
    { initialState
    , render
    , eval: H.mkEval H.defaultEval
    }
  where
  initialState :: ButtonInput -> ButtonState
  initialState { label } = { label }

  render :: forall action. ButtonState -> H.ComponentHTML action () m
  render { label } = HH.button_ [ HH.text label ]

If you load this into Try PureScript you'll see that our button...never changes! Even though the parent component is sending it new input every second (every time the parent re-renders) our child component is never receiving it. It's not enough to accept input; we also need to explicitly decide what to do each time it is received.

Try replacing the button code with this revised code to see the difference:

data ButtonAction = Receive ButtonInput

type ButtonInput = { label :: String }

type ButtonState = { label :: String }

button :: forall query output m. H.Component query ButtonInput output m
button =
  H.mkComponent
    { initialState
    , render
    , eval: H.mkEval $ H.defaultEval
        { handleAction = handleAction
        , receive = Just <<< Receive
        }
    }
  where
  initialState :: ButtonInput -> ButtonState
  initialState { label } = { label }

  render :: ButtonState -> H.ComponentHTML ButtonAction () m
  render { label } = HH.button_ [ HH.text label ]

  handleAction :: ButtonAction -> H.HalogenM ButtonState ButtonAction () output m Unit
  handleAction = case _ of
    -- When we receive new input we update our `label` field in state.
    Receive input ->
      H.modify_ _ { label = input.label }

We made several changes in the new version to ensure we stayed up-to-date with input from the parent component:

  1. We added a new action, Receive, a constructor that accepts the Input type as its argument. We then handled this action in our handleAction function by updating our state when new input is received.
  2. We added a new field to our eval spec, receive, which holds a function that will be called every time new input is received. Our function returns our Receive action so it can be evaluated.

This change is sufficient to subscribe our child component to new input from the parent component. You should now see that our button's label updates every second. As an exercise, you can replace our receive function with const Nothing to see the how the input is ignored once again.

Output Messages

Sometimes an event happens in a child component that it shouldn't handle itself.

For example, let's say we're writing a modal component, and we need to evaluate some code when a user clicks to close the modal. To keep this modal flexible we'd like for the parent component to decide what should happen when the modal is closed.

In Halogen we'd handle this situation by designing the modal (the child component) to raise an output message to the parent component. The parent component can then handle the message like any other action in its handleAction function. Conceptually, it's as though the child component is a subscription that the parent component automatically subscribes to.

Concretely, our modal could raise a Closed output to the parent component. The parent could then change its state to indicate the modal should no longer display, and on the next render the modal is removed from the DOM.

As a tiny example, let's consider how we'd design a button that lets the parent component decide what to do when it is clicked:

module Button where

-- This component can notify parent components of one event, `Clicked`
data Output = Clicked

-- This component can handle one internal event, `Click`
data Action = Click

-- Our output type shows up in our `Component` type
button :: forall query input m. H.Component query input Output m
button =
  H.mkComponent
    { initialState: identity
    , render
    , eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
    }
  where
  render _ =
    HH.button
      [ HE.onClick \_ -> Click ]
      [ HH.text "Click me" ]

  -- Our output type also shows up in our `HalogenM` type, because this is
  -- where we can emit these output messages.
  handleAction :: forall state. Action -> H.HalogenM state Action () Output m Unit
  handleAction = case _ of
    -- When the button is clicked we notify the parent component that the
    -- `Clicked` event has happened by emitting it with `H.raise`.
    Click ->
      H.raise Clicked

We took a few steps to implement this output message.

  1. We added an Output type which describes what output messages our component can emit. We used the type in our Component type because it's part of the component's public interface and our HalogenM type because this is where we can actually emit the output message.
  2. We added an Action type with a Click constructor to handle the click event in our Halogen HTML
  3. We handled the Click action in our handleAction by raising an output message to the parent component. You can emit output messages with the H.raise function.

We now know how a component can emit output messages. Now, let's see how to handle output messages from a child component. There are three things to keep in mind:

  1. When you render a child component you will need to add it to your slots type, which is then used in your ComponentHTML and HalogenM types. The type you add will include the child component's output message type, which allows the compiler to verify your handler.
  2. When you render a child component with the slot function you can provide an action that should be evaluated when new output arises. This is similar to how lifecycle functions like initialize accept an action to evaluate when the component initializes.
  3. Then, you'll need to add a case to your handleAction for the action you added to handle the child component's output.

Let's start writing our parent component by writing a slot type:

module Parent where

import Button as Button

type Slots = ( button :: forall query. H.Slot query Button.Output Int )

-- We can refer to the `button` label using a symbol proxy, which is a
-- way to refer to a type-level string like `button` at the value level.
-- We define this for convenience, so we can use _button to refer to its
-- label in the slot type rather than write `Proxy` over and over.
_button = Proxy :: Proxy "button"

Our slot type is a row, where each label designates a particular type of child component we support, in each case using the type H.Slot:

H.Slot query output id

This type records the queries that can be sent to this type of component, the output messages that we can handle from the component, and a type we can use to uniquely identify an individual component.

Consider, for example, that we could render 10 of these button components -- how would you know which one to send a query to? That's where the slot id comes into play. We'll learn more about that when we discuss queries.

Our parent component's row type makes it clear that we can support one type of child component, which we can reference with the symbol button and an identifier of type Int. We can't send queries to this component because the type variable was left open. But it can send us outputs of type Button.Output.

Next, we need to provide an action for handling these outputs:

data Action = HandleButton Button.Output

When this action occurs in our component, we can unwrap it to get the Button.Output value and use that to decide what code to evaluate. Now that we have our slot and action types handled, let's write our parent component:

parent :: forall query input output m. H.Component query input output m
parent =
  H.mkComponent
    { initialState: identity
    , render
    , eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
    }
  where
  render :: forall state. state -> H.ComponentHTML Action Slots m
  render _ =
    HH.div_
      [ HH.slot _button 0 button unit HandleButton ]

  handleAction :: forall state. Action -> H.HalogenM state Action Slots output m Unit
  handleAction = case _ of
    HandleButton output ->
      case output of
        Button.Clicked -> do
          ...

You'll notice that our Slots type has now been used in both the ComponentHTML type and the HalogenM type. Also, this component is now notified any time the Button.Clicked event happens in the child component, which lets the parent component evaluate whatever code it wants in response.

And that's it! You now know how to raise output messages from a child component to a parent component and how to then handle those messages in the parent component. This is the primary way a child component can communicate with a parent component. Now let's see how a parent component can send information to a child component.

Queries

Queries represent commands or requests that a parent component can send to a child component. They're similar to actions and are handled with a handleQuery function similar to the handleAction function. But they arise from outside the component, instead of internally within the component as actions are, which means they are part of the public interface of a component.

Queries are most useful when a parent component needs to control when an event occurs instead of a child component. For example:

  • A parent component can tell a form to submit, rather than wait for a user to click a submit button.
  • A parent component can request the current selections from an autocomplete, rather than wait for an output message from the child component when a selection is made.

Queries are a way for parent components to imperatively control a child component. As introduced in our two examples, there are two common styles of query: a tell-style query for when a parent component commands a child component to do something, and a request-style query for when a parent component wants information from a child component.

The parent component can send a query, but the child component defines the query and also handles the query. That makes queries similar conceptually to actions: just like how you define an Action type and handle actions for your component with handleAction, you define a Query type and a handleQuery function for queries.

Here's a brief example of a query type that includes a tell-style and request-style query:

data Query a
  = Tell a
  | Request (Boolean -> a)

We can interpret this query as meaning "A parent component can tell this component to do something with the tell function and it can request a Boolean from this component with the request function." When you implement a query type, remember that the a type parameter should be present in every constructor. It should be the final argument for tell-style queries and be the result of a function type for request-style queries.

Queries are handled with a handleQuery function in your eval spec, just like how actions are handled with a handleAction function. Let's write a handleQuery function for our custom data type, assuming some state, action, and output types have already been defined:

handleQuery :: forall a m. Query a -> H.HalogenM State Action () Output m (Maybe a)
handleQuery = case _ of
  Tell a ->
    -- ... do something, then return the `a` we received
    pure (Just a)

  Request reply ->
    -- ... do something, then provide the requested `Boolean` to the `reply`
    -- function to produce the `a` we need to return
    pure (Just (reply true))

The handleQuery function takes a query of type Query a and produces some HalogenM code that returns Maybe a. This is why each constructor of our query type needs to contain an a: we need to return it in handleQuery.

When we receive a tell-style query we can just wrap the a we received in Just to return it, as we did to handle the Tell a case in handleQuery.

When we receive a request-style query, though, we have to do a little more work. Instead of receiving an a value we can return, we receive a function that will give us an a that we can then return. For example, in our Request (Boolean -> a) case, we receive a function that will give us an a when we apply it to a Boolean. By convention this function is called reply when you pattern match on a request-style query. In handleQuery we gave this function true to get an a, then wrapped the a in Just to return it.

Request-style queries may look strange at first. But the style allows our query type to return many types of values instead of only one type of value. Here are a few different request types that return different things:

data Requests a
  = GetInt (Int -> a)
  | GetRecord ({ a :: Int, b :: String } -> a)
  | GetString (String -> a)
  | ...

A parent component can use GetInt to retrieve an Int from our component, GetString to retrieve a String from our component, and so on. You can consider a the type returned by the query type, and request-style queries a way to let a be many different possible types. In a moment we'll see how to do this from a parent component.

Let's see another tiny example that demonstrates how to define and handle queries in a component.

-- This component can be told to increment or can answer requests for
-- the current count
data Query a
  = Increment a
  | GetCount (Int -> a)

type State = { count :: Int }

-- Our query type shows up in our `Component` type
counter :: forall input output m. H.Component Query input output m
counter =
  H.mkComponent
    { initialState: \_ -> { count: 0 }
    , render
    , eval: H.mkEval $ H.defaultEval { handleQuery = handleQuery }
    }
  where
  render { count } =
    HH.div_
      [ HH.text $ show count ]

  -- We write a function to handle queries when they arise.
  handleQuery :: forall action a. Query a -> H.HalogenM State action () output m (Maybe a)
  handleQuery = case _ of
    -- When we receive the `Increment` query we'll increment our state.
    Increment a -> do
      H.modify_ \state -> state { count = state.count + 1 }
      pure (Just a)

    -- When we receive the `GetCount` query we'll respond with the state.
    GetCount reply -> do
      { count } <- H.get
      pure (Just (reply count))

In this example we've defined a counter that lets the parent tell it to increment or request its current count. To do this, we:

  1. Implemented a query type that includes a tell-style query, Increment a, and a request-style query, GetCount (Int -> a). We added this query type to our component's public interface, Component.
  2. Implemented a query handler, handleQuery, that runs code when these queries arise. We'll add this to our eval.

We now know how to define queries and evaluate them in a child component. Now, let's see how to send a query to a child component from a parent component. As usual, we can start by defining our parent component's slot type:

module Parent where

type Slots = ( counter :: H.Slot Counter.Query Void Int )

_counter = Proxy :: Proxy "counter"

Our slot type records the counter component with its query type and leaves its output message type as Void to indicate there are none.

When our parent component initializes, we'll fetch the count from the child component, then increment it, and then get the count again so we can see that it has increased. To do that, we'll need an action to run on initialize:

data Action = Initialize

Now, we can move on to our component definition.

parent :: forall query input output m. H.Component query input output m
parent =
  H.mkComponent
    { initialState: identity
    , render
    , eval: H.mkEval $ H.defaultEval
        { handleAction = handleAction
        , initialize = Just Initialize
        }
    }
  where
  render :: forall state. state -> H.ComponentHTML Action Slots m
  render _ =
    HH.div_
      [ HH.slot_ _counter unit counter unit ]

  handleAction :: forall state. Action -> H.HalogenM state Action Slots output m Unit
  handleAction = case _ of
    Initialize ->
      -- startCount :: Maybe Int
      startCount <- H.request _counter unit Counter.GetCount
      -- _ :: Maybe Unit
      H.tell _counter unit Counter.Increment
      -- endCount :: Maybe Int
      endCount <- H.request _counter unit Counter.GetCount

      when (startCount /= endCount) do
        -- ... do something

There are several things to notice here.

  1. We used the proxy for the counter's label in the slot type, _counter, along with its identifier, unit, both to render the component with the slot function and also to send queries to the component with the tell and request functions. The label and identifier are always used to work with a particular child component.
  2. We used the H.tell function to send the tell-style query Increment, and we used the H.request function to send the request-style query GetCount. The GetCount query had a reply function of type (Int -> a), so you'll notice that when we used it we received a Maybe Int in return.

The tell and request functions take a label, a slot identifier, and a query to send. The tell function doesn't return anything, but the request function returns a response from the child wrapped in Maybe, where Nothing signifies that the query failed (either the child component returned Nothing, or no component exists at the label and slot identifier you provided). There are also tellAll and requestAll functions that send the same query to all components at a given label.

Many people find queries to be the most confusing part of the Halogen library. Luckily, queries aren't used nearly so much as the other Halogen features we've learned about in this guide, and if you get stuck you can always return to this section of the guide as a reference.

Component Slots

We've learned a lot about how components communicate with one another. Before we move on to our final example let's recap what we've learned about slots along the way.

A component needs to know what types of child component its supports so that it's able to communicate with them. It needs to know what queries it can send to them and what output messages it can receive from them. It also needs to know how to identify which particular component to send a query to.

The H.Slot type captures the queries, outputs, and unique identifier for a particular type of child component the parent component can support. You can combine many slots together into a row of slots, where each label is used for a particular type of component. Here's how you could read the type definitions for a few different slots:

type Slots = ()

This means the component supports no child components.

type Slots = ( button :: forall query. H.Slot query Void Unit )

This means the component supports one type of child component, identified by the symbol button. You can't send queries to it (because q is an open type variable) and it doesn't emit any output messages (usually represented with Void so you can use absurd as the handler). You can have at most one of this component because only one value, unit, inhabits the Unit type.

type Slots = ( button :: forall query. H.Slot query Button.Output Int )

This type is quite similar to previous one. The difference is that the child component can raise output messages of type Button.Output, and you can have as many of this component as there are integers.

type Slots =
  ( button :: H.Slot Button.Query Void Int
  , modal :: H.Slot Modal.Query Modal.Output Unit
  )

This slot type means the component supports two types of child component, identified by the labels button and modal. You can send queries of type Button.Query to the button component, and you won't receive any output messages from it. You can send queries of type Modal.Query to and receive messages of type Modal.Output from the modal component. You can have as many of the button component as there are integers, but at most one modal component.

A common pattern in Halogen apps is for a component to export its own slot type, because it already knows its query and messages types, without exporting the type that identifies this particular component because that's the parent's responsibility.

For example, if the button and modal component modules exported their own slot types, like this:

module Button where

type Slot id = H.Slot Query Void id

module Modal where

type Slot id = H.Slot Query Output id

Then our last slot type example would become this simpler type:

type Slots =
  ( button :: Button.Slot Int
  , modal :: Modal.Slot Unit
  )

This has the advantage of being more concise and easier to keep up-to-date over time, as if there are changes to the slot type they can happen in the source module instead of everywhere the slot type is used.

Full Example

To wrap up, we've written an example of a parent and child component using all the communication mechanisms we've discussed in this chapter. The example is annotated with how we'd interpret the most important lines of code -- what we'd glean by skimming through these component definitions in our own codebases.

As usual, we suggest pasting this code into Try PureScript so you can explore it interactively.

module Main where

import Prelude

import Data.Maybe (Maybe(..))
import Effect (Effect)
import Effect.Class (class MonadEffect)
import Effect.Class.Console (logShow)
import Halogen as H
import Halogen.Aff as HA
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.VDom.Driver (runUI)
import Type.Proxy (Proxy(..))

main :: Effect Unit
main = HA.runHalogenAff do
  body <- HA.awaitBody
  runUI parent unit body

-- The parent component supports one type of child component, which uses the
-- `ButtonSlot` slot type. You can have as many of this type of child component
-- as there are integers.
type Slots = ( button :: ButtonSlot Int )

-- The parent component can only evaluate one action: handling output messages
-- from the button component, of type `ButtonOutput`.
data ParentAction = HandleButton ButtonOutput

-- The parent component maintains in local state the number of times all its
-- child component buttons have been clicked.
type ParentState = { clicked :: Int }

-- The parent component uses no query, input, or output types of its own. It can
-- use any monad so long as that monad can run `Effect` functions.
parent :: forall query input output m. MonadEffect m => H.Component query input output m
parent =
  H.mkComponent
    { initialState
    , render
      -- The only internal event this component can handle are actions as
      -- defined in the `ParentAction` type.
    , eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
    }
  where
  initialState :: input -> ParentState
  initialState _ = { clicked: 0 }

  -- We render three buttons, handling their output messages with the `HandleButton`
  -- action. When our state changes this render function will run again, each time
  -- sending new input (which contains a new label for the child button component
  -- to use.)
  render :: ParentState -> H.ComponentHTML ParentAction Slots m
  render { clicked } = do
    let clicks = show clicked
    HH.div_
      [ -- We render our first button with the slot id 0
        HH.slot _button 0 button { label: clicks <> " Enabled" } HandleButton
        -- We render our second button with the slot id 1
      , HH.slot _button 1 button { label: clicks <> " Power" } HandleButton
        -- We render our third button with the slot id 2
      , HH.slot _button 2 button { label: clicks <> " Switch" } HandleButton
      ]

  handleAction :: ParentAction -> H.HalogenM ParentState ParentAction Slots output m Unit
  handleAction = case _ of
    -- We handle one action, `HandleButton`, which itself handles the output messages
    -- of our button component.
    HandleButton output -> case output of
      -- There is only one output message, `Clicked`.
      Clicked -> do
        -- When the `Clicked` message arises we will increment our clicked count
        -- in state, then send a query to the first button to tell it to be `true`,
        -- then send a query to all the child components requesting their current
        -- enabled state, which we log to the console.
        H.modify_ \state -> state { clicked = state.clicked + 1 }
        H.tell _button 0 (SetEnabled true)
        on <- H.requestAll _button GetEnabled
        logShow on

-- We now move on to the child component, a component called `button`.

-- This component can accept queries of type `ButtonQuery` and send output
-- messages of type `ButtonOutput`. This slot type is exported so that other
-- components can use it when constructing their row of slots.
type ButtonSlot = H.Slot ButtonQuery ButtonOutput

-- We think our button will have the label "button" in the row where it's used,
-- so we're exporting a symbol proxy for convenience.
_button = Proxy :: Proxy "button"

-- This component accepts two queries. The first is a request-style query that
-- lets a parent component request a `Boolean` value from us. The second is a
-- tell-style query that lets a parent component send a `Boolean` value to us.
data ButtonQuery a
  = GetEnabled (Boolean -> a)
  | SetEnabled Boolean a

-- This component can notify parent components of one event, `Clicked`
data ButtonOutput
  = Clicked

-- This component can handle two internal actions. It can evaluate a `Click`
-- action and it can receive new input when its parent re-renders.
data ButtonAction
  = Click
  | Receive ButtonInput

-- This component accepts a label as input
type ButtonInput = { label :: String }

-- This component stores a label and an enabled flag in state
type ButtonState = { label :: String, enabled :: Boolean }

-- This component supports queries of type `ButtonQuery`, requires input of
-- type `ButtonInput`, and can send outputs of type `ButtonOutput`. It doesn't
-- perform any effects, which we can tell because the `m` type parameter has
-- no constraints.
button :: forall m. H.Component ButtonQuery ButtonInput ButtonOutput m
button =
  H.mkComponent
    { initialState
    , render
      -- This component can handle internal actions, handle queries sent by a
      -- parent component, and update when it receives new input.
    , eval: H.mkEval $ H.defaultEval
        { handleAction = handleAction
        , handleQuery = handleQuery
        , receive = Just <<< Receive
        }
    }
  where
  initialState :: ButtonInput -> ButtonState
  initialState { label } = { label, enabled: false }

  -- This component has no child components. When the rendered button is clicked
  -- we will evaluate the `Click` action.
  render :: ButtonState -> H.ComponentHTML ButtonAction () m
  render { label, enabled } =
    HH.button
      [ HE.onClick \_ -> Click ]
      [ HH.text $ label <> " (" <> (if enabled then "on" else "off") <> ")" ]

  handleAction
    :: ButtonAction
    -> H.HalogenM ButtonState ButtonAction () ButtonOutput m Unit
  handleAction = case _ of
    -- When we receive new input we update our `label` field in state.
    Receive input ->
      H.modify_ _ { label = input.label }

    -- When the button is clicked we update our `enabled` field in state, and
    -- we notify our parent component that the `Clicked` event happened.
    Click -> do
      H.modify_ \state -> state { enabled = not state.enabled }
      H.raise Clicked

  handleQuery
    :: forall a
     . ButtonQuery a
    -> H.HalogenM ButtonState ButtonAction () ButtonOutput m (Maybe a)
  handleQuery = case _ of
    -- When we receive a the tell-style `SetEnabled` query with a boolean, we
    -- set that value in state.
    SetEnabled value next -> do
      H.modify_ _ { enabled = value }
      pure (Just next)

    -- When we receive a the request-style `GetEnabled` query, which requires
    -- a boolean result, we get a boolean from our state and reply with it.
    GetEnabled reply -> do
      enabled <- H.gets _.enabled
      pure (Just (reply enabled))

In the next chapter we'll learn more about running Halogen applications.

Running an Application

Over the course of this guide we've seen the standard way to run a Halogen application several times. In this chapter, we'll learn what is actually going on when we run a Halogen application and how to control a running app from the outside.

Using runUI and awaitBody

PureScript applications use the main function in their Main module as their entrypoint. Here's a standard main function for Halogen apps:

module Main where

import Prelude

import Effect (Effect)
import Halogen.Aff as HA
import Halogen.VDom.Driver (runUI)

main :: Effect Unit
main = HA.runHalogenAff do
  body <- HA.awaitBody
  runUI component unit body

-- Assuming you have defined a root component for your application
component :: forall query input output m. H.Component query input output m
component = ...

The most important function used in main is the runUI function. Provide runUI with your root component, the root component's input value, and a reference to a DOM element, and it will provide your application to the Halogen virtual DOM. The virtual DOM will then render your application at that element and maintain it there for as long as your app is running.

runUI
  :: forall query input output
   . Component query input output Aff
  -> input
  -> DOM.HTMLElement
  -> Aff (HalogenIO query output Aff)

As you can see, the runUI function requires that your Halogen application can ultimately be run in the Aff monad. In this guide we used constraints like MonadEffect and MonadAff, which Aff satisfies, so we're in the clear.

If you chose to use another monad for your application then you'll need to hoist it to run in Aff before you provide your application to runUI. The Real World Halogen uses a custom AppM monad that serves as a good example of how to do this.

In addition to runUI we used two other helper functions. First, we used awaitBody to wait for the page to load and then acquire a reference to the <body> tag as the root HTML element for the application to control. Second, we used runHalogenAff to launch asynchronous effects (our Aff code containing awaitBody and runUI) from within Effect. This is necessary because awaitBody, runUI, and our applications run in the Aff monad, but PureScript main functions must be in Effect.

The main function we've used here is the standard way to run a Halogen application that is the only thing running on the page. Sometimes, though, you may use Halogen to take over just one part of the page, or you may be running multiple Halogen apps. In these cases, you'll probably reach for a pair of different helper functions:

  1. awaitLoad blocks until the document has loaded so that you can safely retrieve references to HTML elements on the page
  2. selectElement can be used to target a particular element on the page to embed the app within

Using HalogenIO

When you run your Halogen application with runUI you receive a record of functions with the type HalogenIO. These functions can be used to control your root component from outside the application. Conceptually, they're like a makeshift parent component for your application.

type HalogenIO query output m =
  { query :: forall a. query a -> m (Maybe a)
  , messages :: Event output
  , dispose :: m Unit
  }
  1. The query function is like the H.query function which underpins tell and request. This allows you to send queries to the root component of your application from outside the application.
  2. The messages event can be used to subscribe to a stream of output messages from the component -- it's like the handler we provided to the slot function, except rather than evaluate an action here we can perform some effect instead.
  3. The dispose function can be used to halt and clean up the Halogen application. This will kill any forked threads, close all subscriptions, and so on.

You can't use tell and request at the root of your application, but you can use the mkTell and mkRequest functions (as seen in the example below) for a similar effect.

A common pattern in Halogen applications is to use a Route component as the root of the application, and use the query function from HalogenIO to trigger route changes in the application when the URL changes. You can see a full example of doing this in the Real World Halogen Main.purs file.

Full Example: Controlling a Button With HalogenIO

You can paste this example into Try PureScript to explore using HalogenIO to control the root component of an application.

module Example.Driver.IO.Main where

import Prelude

import Data.Maybe (Maybe(..))
import Effect (Effect)
import Effect.Console (log)
import Halogen (liftEffect)
import Halogen as H
import Halogen.HTML as HH
import Halogen.Aff as HA
import Halogen.HTML.Events as HE
import Halogen.HTML.Properties as HP
import Halogen.Subscription as HS
import Halogen.VDom.Driver (runUI)

main :: Effect Unit
main = HA.runHalogenAff do
  body <- HA.awaitBody
  io <- runUI component unit body

  _ <- liftEffect $ HS.subscribe io.messages \(Toggled newState) -> do
    liftEffect $ log $ "Button was internally toggled to: " <> show newState
    pure Nothing

  state0 <- io.query $ H.mkRequest IsOn
  liftEffect $ log $ "The button state is currently: " <> show state0

  void $ io.query $ H.mkTell (SetState true)

  state1 <- io.query $ H.mkRequest IsOn
  liftEffect $ log $ "The button state is now: " <> show state1

-- Child component implementation

type Slot = H.Slot Query Message

data Query a
  = IsOn (Boolean -> a)
  | SetState Boolean a

data Message = Toggled Boolean

data Action = Toggle

type State = { enabled :: Boolean }

component :: forall i m. H.Component Query i Message m
component =
  H.mkComponent
    { initialState
    , render
    , eval: H.mkEval $ H.defaultEval
        { handleAction = handleAction
        , handleQuery = handleQuery
        }
    }

initialState :: forall i. i -> State
initialState _ = { enabled: false }

render :: forall m. State -> H.ComponentHTML Action () m
render state =
  let
    label = if state.enabled then "On" else "Off"
  in
    HH.button
      [ HP.title label
      , HE.onClick \_ -> Toggle
      ]
      [ HH.text label ]

handleAction :: forall m. Action -> H.HalogenM State Action () Message m Unit
handleAction = case _ of
  Toggle -> do
    newState <- H.modify \st -> st { enabled = not st.enabled }
    H.raise (Toggled newState.enabled)

handleQuery :: forall m a. Query a -> H.HalogenM State Action () Message m (Maybe a)
handleQuery = case _ of
  IsOn k -> do
    enabled <- H.gets _.enabled
    pure (Just (k enabled))
  SetState enabled a -> do
    H.modify_ (_ { enabled = enabled })
    pure (Just a)

Next Steps

This guide has demonstrated the basic building blocks for Halogen applications. We learned how Halogen provides a type-safe, declarative way to build complex apps out of reusable pieces called components. We learned how write functions that produce Halogen HTML, how to write individual components, and how to render components within other components. We also learned how components can communicate with each other and how to run a full Halogen application.

You now know how Halogen works, but you may not yet feel comfortable building a real application with the library yet. That's perfectly normal! There are more resources to help you continue learning about Halogen.

  1. To go more in-depth on concepts you learned in this guide, explore the Concepts Reference.
  2. To learn Halogen in a slower-paced, bottom-up way, try reviewing Jordan Martinez's Learn Halogen repository.
  3. To learn how to build real world applications in Halogen, review the Real World Halogen handbook and example application.

Halogen Concepts Reference

Halogen is a declarative, component-based UI library for PureScript that emphasizes type safety. This concepts reference is a glossary of the concepts used in Halogen, along with their technical motivation.

This reference is still in progress. Check back later to see the finished product! For now, we suggest reading through the Halogen Guide to learn Halogen.

Halogen Changelog

Halogen's major versions come with transition guides that explain how to migrate your code from one version to the next, along with summaries and the motivation for major changes to the library.

Currently these major versions are supported:

Changes in v6

This is a crash-course on the changes from Halogen 5 to Halogen 6. Please open an issue or PR if you notice missing information or ways this guide could be improved!

Halogen 6 introduces several quality-of-life improvements for using Halogen on a day-to-day basis, without major changes to how you use the library to build your applications. It's an intentionally small release which adds polish to the library and which is the first version to support version 0.14 of the PureScript compiler.

If you are migrating an application from Halogen 5 we recommend reading through the full transition guide. However, you can also hop directly to a relevant section using the table of contents below.

  1. PureScript 0.14
  2. Component Types
  3. Event Handler Types
  4. Query Helper Functions
  5. Subscriptions
  6. Other Changes

PureScript 0.14

Halogen 6 is the first version of Halogen compatible with PureScript 0.14. You'll need PureScript 0.14 to compile the library, and if you're upgrading your application to use PureScript 0.14 then you'll need to be on Halogen 6. We know it can be painful dealing with compiler changes and library changes, so we've kept this release intentionally small.

Component Types

Component types have been simplified by removing the surface parameter.

In Halogen 5 (and prior versions), components and the internal functions which manage them carried a surface parameter which indicated the target for the UI to render. As no one ever wrote an alternate target from HTML, Halogen applications have always fixed this parameter to HTML in component definitions, as in:

import Halogen as H
import Halogen.HTML as HH

myComponent :: forall q i o m. H.Component HH.HTML q i o m

In Halogen 6 the surface parameter has been removed. The only real user-visible change is that components and functions which operate on them no longer carry the surface parameter.

import Halogen as H

myComponent :: forall q i o m. H.Component q i o m

This is a breaking change, but one which is easily fixed: remove this parameter from your components and any related functions and types.

Added in #616.

Event Handler Types

We've also made event handlers a little nicer to work with. The Maybe action return value has been removed from event handlers, which now return action directly.

In Halogen 5, when you wanted to respond to a click event, or a message from a child component, the output was of type Maybe action. This allowed you to selectively emit outputs (you could emit Nothing for some events). In practice, though, few do this.

To remove friction around such a common task, these handlers no longer return in Maybe in Halogen 6. Instead, they return the action directly. Here is how some simple render code would change from Halogen 5 to Halogen 6:

 HH.div_
   [ HH.button
-      [ HE.onClick \_ -> Just Clear ]
+      [ HE.onClick \_ -> Clear ]
       [ HH.text "Clear" ]
-   , HH.slot _id unit component unit (Just <<< Handle)
+   , HH.slot _id unit component unit Handle
   ]

You're no longer able to ignore the output of a child component by providing a handler \_ -> Nothing. Instead, you can use the slot_ function if you don't care about a child component's output. This code from Halogen 5:

HH.slot _id unit component unit (\_ -> Nothing)

becomes this code in Halogen 6:

HH.slot_ _id unit component unit

Note: You can recover the old Halogen 5 behavior by adding a DoNothing constructor to your action type, or by wrapping your action type in Maybe.

Added in #636 and #642.

Query Helper Functions

We've simplified the helper functions which are used with queries so that you can use tell and request directly, rather than use them in conjunction with the query and request functions.

In Halogen 5, to execute a query you would use the query function and combine it with the request function (for request-style queries, which return a result) or the tell function (for tell-style queries, which don't return a result). This was always a bit difficult to explain and easy to trip over when writing queries yourself.

Here's how you would execute a request-style and then a tell-style query in Halogen 5:

handleAction = do
  a <- H.query _a unit (H.request Child.SomeRequestQuery)
  _ <- H.query _a unit (H.tell Child.SomeTellQuery)

In Halogen 6, you no longer use the query function. Instead, you use request and tell directly. You also don't have to throw away the result of tell, as it can already be safely discarded:

handleAction = do
  a <- H.request _a unit Child.SomeRequestQuery
  H.tell _a unit Child.SomeTellQuery

The old tell and request functions still exist in Halogen 6, but they've been renamed to mkTell and mkRequest and are only used when querying the root of your application. For example, this code in Halogen 5:

io <- runUI component unit body

state <- io.query $ H.request SomeRequestQuery
_ <- io.query $ H.tell SomeTellQuery

becomes this code in Halogen 6:

io <- runUI component unit body

state <- io.query $ H.mkRequest SomeRequestQuery
_ <- io.query $ H.mkTell SomeTellQuery

Added in #621.

Subscriptions

Event sources have been replaced with the new halogen-subscriptions library. The previous implementation of event sources was built on top of coroutines. This update simplifies the library internals and connects Halogen with a subscription management library that can be used independently of Halogen itself.

Notable changes include:

  • The entire Halogen.Query.EventSource module has been removed and replaced with Halogen.Query.Event which provides only an eventListener function. The new function is a drop-in replacement for the old eventListenerEventSource, so all you need to do is update your import.
  • affEventSource and effectEventSource functions can be trivially replaced with code using the halogen-subscriptions library directly, so they have been removed. Examples of how to rewrite these functions are below.
  • The other helper functions and types from the Halogen.Query.EventSource module are no longer required.
  • The subscribe function and Subscribe constructor no longer take an EventSource m action to subscribe to. They take an Emitter action instead.
  • The HalogenIO type returned by running your root-level component now contains a messages :: Emitter output instead of a subscribe :: Coroutine.Consumer output m Unit -> m Unit.

If you were previously using effectEventSource, then you would change this Halogen 5 code:

import Halogen as H
import Halogen.Query.EventSource as ES

do
  void $ H.subscribe $ ES.effectEventSource \emitter -> do
    ES.emit emitter MyAction
    pure mempty

with this Halogen 6 code:

import Halogen as H
import Halogen.Subscription as HS

do
  { emitter, listener } <- H.liftEffect HS.create
  void $ H.subscribe emitter
  H.liftEffect $ HS.notify listener MyAction

When running your root component, you'll also need to replace your use of coroutines. For example, this Halogen 5 code:

main :: Effect Unit
main = ...
  io <- runUI component unit body
  io.subscribe $ Coroutine.consumer \msg -> do
     ...

should be replaced with this Halogen 6 code:

main :: Effect Unit
main = ...
  io <- runUI component unit body
  _ <- liftEffect $ HS.subscribe io.messages \msg -> do
     ...

Other changes

Halogen 6 is an intentionally small release because it coincides with the PureScript 0.14 release. There are only a few other changes in the library to report:

  • The id_ function has been renamed to id now that id has been renamed to identity in the PureScript Prelude. id_ continues to work, but has a deprecation notice and will be removed in the next version. See #717.
  • PureScript 0.14 deprecated the SProxy type in favor of the simpler Proxy type. For this reason, all usages of SProxy in Halogen have been replaced with Proxy. You can do the same in your application with a simple find/replace which replaces SProxy with Proxy and the import Data.Symbol (SProxy(..)) with Type.Proxy (Proxy(..)).
  • The AttrName, PropName, and ClassName types from Halogen have been migrated into the web-html library and imported back into Halogen. This allows libraries to share these types rather than re-implement them over and over. These types are re-exported from Halogen, so your code doesn't need to change.

Changes in v5

This is a crash-course guide to things that have changed from Halogen 4 to Halogen 5. Please open an issue or a PR if you notice missing information or ways this transition guide could be improved!

Halogen 5 introduces many improvements to Halogen's performance and usability. If you are migrating an application from Halogen 4 we recommend reading through the full transition guide. However, you can also hop directly to a relevant section using the table of contents below.

  1. Component Constructors, HTML, and DSL Types
  2. Queries and Actions
  3. Component Evaluation
  4. Child Component Addressing
  5. Subscriptions, Forking, and Event Sources
  6. Performance Optimization with Lazy and Memoized
  7. Other Changes

Component Constructors, HTML, and DSL Types

Halogen 4 distinguished among parent- and child-specific for the HTML and DSL types used when defining a component, and between parent-, child-, and lifecycle-specific functions for constructing components.

Halogen 5 uses only one component constructor function, mkComponent, one type for HTML, ComponentHTML, and one type for component evaluation, HalogenM.

For example, a parent component would previously be defined with the parentComponent constructor and use the ParentHTML and ParentDSL type synonyms:

parentComponent :: H.Component HH.HTML Query Input Message m
parentComponent =
  H.parentComponent
    ...
  where
  render :: State  -> H.ParentHTML Query ChildQuery Slots m

  eval
    :: Query
    ~> H.ParentDSL State Query ChildQuery Slots Message m

Whereas a child component would be defined with the component constructor and use the ComponentHTML and ComponentDSL type synonyms:

childComponent :: H.Component HH.HTML Query Input Message m
childComponent =
  H.component
    ...
  where
  render :: State -> H.ComponentHTML Query

  eval :: Query ~> H.ComponentDSL State Query Message m

A component which used lifecycles (an initializer and/or finalizer) would be constructed with yet another pair of constructor functions:

parentComponentWithLifecycles = H.lifecycleParentComponent ...
childComponentWithLifecycles = H.lifecycleComponent ...

In Halogen 5, the only component constructor is mkComponent, the only type for HTML is ComponentHTML, and the only type for component evaluation is HalogenM.

Due to changes in queries and evaluation in Halogen 5, these types are not the same as they were in Halogen 4. We'll explore those changes in the next section.

Queries and Actions

In Halogen 4, a component's query algebra defines everything the component can do. In Halogen 5, queries are only for parent-child communication, and a simpler action type is used within the component.

Previously, queries were the only type for defining computations the component can run. Queries were paired with the eval function, which defines the computation that should run when a query happens. There were two ways to write a query: "action-style" and "request-style":

data Query a
  = HandleClick a
  | RespondWithInt (Int -> a)

Action-style queries like HandleClick don't return anything when they are run by the eval function, whereas request-style queries like RespondWithInt do return a result. Correspondingly, action-style queries were typically used to handle events arising from HTML or event sources, and request-style queries were used for parent-child component communication.

In Halogen 5 this distinction has been made explicit. Components now use two separate types to represent computations: a query type for parent-child communication and an action type for internal events (like those arising from HTML or event sources).

The above query type from Halogen 4 would become, in Halogen 5, these two definitions:

-- Actions don't need to be parameterised because they can't
-- return a value. Actions are used instead of queries in
-- ComponentHTML and to handle event sources.
data Action
  = HandleClick

-- Queries are the same as they were in Halogen 4, but are
-- used specifically for parent-child communication instead of
-- being used to represent all computations in a component.
data Query a
  = RespondWithInt (Int -> a)

Actions don't show up in the type of the component because they cannot be accessed outside of the component:

component :: forall m. H.Component Query Input Output m

Changes to Query Evaluation

Queries are still used as the public interface for a component, which means they are useful for parent-child communication. They aren't required, however: many components are self-contained and only need actions.

There have been a few other tweaks to queries in Halogen 5 worth knowing about.

You can still write "action-style" queries, but to avoid terminology overloading, they're now termed "tell-style" queries and are constructed using H.tell instead of H.action.

data MyQuery a
  = DoSomething a

-- Halogen 4
result <- H.query ... $ H.action DoSomething

-- Halogen 5
result <- H.query ... $ H.tell DoSomething

In addition, query evaluation in Halogen 5 can now "fail" without resorting to throwing exceptions. Query evaluation in Halogen 5 is now of the type:

query a -> HalogenM ... (Maybe a)

instead of the Halogen 4 type:

query ~> HalogenM ...

If evaluation returns Nothing for a query, then it will be flattened during the call to H.query and become indistinguishible from the case in which the component being queried doesn't exist.

Introducing Actions

Actions are now used to represent computations internal to a component. They are of the kind Type instead of Type -> Type because, unlike queries, they can't return anything.

data Action
  = Increment
  | Decrement

Internally, actions are evaluated similarly to how queries are evaluated, with a function of the type:

action -> HalogenM ... Unit

This action type is now used in place of the query type in your render function:

-- Halogen 4
render :: State -> H.ParentHTML Query ChildQuery Slots m
render :: State -> H.ComponentHTML Query

-- Halogen 5
render :: State -> H.ComponentHTML Action Slots m

We're no longer using Query in the the Halogen 5 version. (We're not using ChildQuery either, but that's unrelated -- that's due to changes in how slots work in Halogen 5, which we'll address in a moment.)

One last thing about actions: since they are not of kind Type -> Type, helper functions like input and input_ are no longer necessary when handling events in HTML, and so they have been removed in Halogen 5

-- Halogen 4
module Halogen.HTML.Events where

type Action f = Unit -> f Unit

input  :: forall f a. (a -> Action f) -> a -> Maybe (f Unit)
input_ :: forall f a. Action f -> a -> Maybe (f Unit)

In Halogen 4 these functions were used to transform queries in the render function:

-- Halogen 4
import Halogen.HTML as HH
import Halogen.HTML.Events as HE

data Query a
  = Toggle a
  | Hover MouseEvent a

render :: State -> H.ComponentHTML Query
render =
  HH.button
    [ HE.onClick (HE.input_ Toggle)
    , HE.onMouseOver (HE.input Hover)
    ]
    [ HH.text "Click me" ]

This is how you'd write the same code in Halogen 5:

-- Halogen 5
data Action
  = Toggle
  | Hover MouseEvent

render :: forall m. State -> H.ComponentHTML Action Slots m
render =
  HH.button
    [ HE.onClick \_ -> Just Toggle
    , HE.onMouseOver (Just <<< Hover)
    ]
    [ HH.text "Click me" ]

Mixing Queries and Actions

Now that actions and queries have been split apart you may want to share some of the behavior between actions and queries without duplicating the constructors and/or implementation. You can do that by adding a constructor to your action type which allows you to use your action-style queries:

data Query a
  = UpdateState a

data Action
  = HandleClick
  | EvalQuery (Query Unit)

Then, you can evaluate the "action-style" query when it arises as an action by unwrapping it and passing it your query evaluation function.

While it's also possible to add an EvalAction Action a constructor to your query type, this isn't recommended. The action type can be used to hide internal interactions that shouldn't be called externally, but the query type is always fully public.

Component Evaluation

Component evaluation has changed now that there is only one constructor, mkComponent, no differentiation between child, parent, and lifecycle components, and an explicit separation between actions and queries.

In Halogen 4, the component constructor had separate fields for the eval function (handling queries) and the receiver function (handling component input), and the lifecycleComponent had additional fields for initializer and finalizer to handle lifecycle events.

In Halogen 5, the mkComponent constructor has just a single evaluation function, eval, which handles all the various kinds of events a component can encounter, including lifecycles, component input, queries, and actions.

eval
  :: HalogenQ query action input
  ~> HalogenM state action slots output m

In a moment we'll examine the eval function in-depth, but in most cases you'll construct it with the mkEval helper function paired with defaultEval, which provides default values for handling each of these cases. If defaultEval is used with no overrides the component will do nothing for any action raised internally, and any queries made of it will fail.

Here are a few different eval functions which handle various cases:

-- This eval function does nothing
H.mkComponent
  { initialState: ...
  , render: ...
  , eval: H.mkEval H.defaultEval
  }

-- This one handles only actions
eval = H.mkEval $ H.defaultEval
  { handleAction = \action - > ...
  }

-- This one handles actions, queries, and initialization:
data Action = Initialize

eval = H.mkEval $ H.defaultEval
  { handleAction = \action -> ...
  , handleQuery = \query -> ...
  , initialize = Just Initialize
  }

As you can tell, the eval function is no longer just for handling queries. Instead, it handles all the cases expressed by HalogenQ, a type that captures the various sorts of input that can be evaluated in a component:

data HalogenQ query action input a
  = Initialize a
  | Finalize a
  | Receive input a
  | Action action a
  | Query (Coyoneda query a) (Unit -> a)

You can write an eval function manually by pattern-matching on each of these constructors, but in most cases you should use the new mkEval helper function. This function accepts a record that looks similar to the old lifecycleComponent constructor:

type EvalSpec state query action slots input output m =
   { handleAction
       :: action
       -> HalogenM state action slots output m Unit
   , handleQuery
       :: forall a
        . query a
       -> HalogenM state action slots output m (Maybe a)
   , receive :: input -> Maybe action
   , initialize :: Maybe action
   , finalize :: Maybe action
   }

The defaultEval function provides default values for each of these handlers, which do nothing, and which you can override using ordinary PureScript record syntax:

-- This eval function uses the defaults, but overrides the
-- `handleAction` and `handleQuery` functions.
eval = H.mkEval $ H.defaultEval
  { handleAction = case _ of ...
  , handleQuery = case _ of ...
  }

Child Component Addressing

Halogen 4 used two types to determine information necessary to render and query child components: the child component query type and a slot value used to identify a particular child component.

These types were unpleasant to work with when a component had multiple types of child component because they required nested Coproduct and Either types to accomodate everything, and you had to remember the order you listed your child component types in when using the slot or query functions.

-- Halogen 4

type ChildQuery =
  Coproduct3
    ComponentA.Query
    ComponentB.Query
    ComponentC.Query

type ChildSlot = Either3 Unit Int Unit

render :: forall m. State -> H.ParentHTML Query ChildQuery ChildSlot m
render state =
  HH.div_
    [ HH.slot' CP.cp1 ComponentA.component unit absurd
    , HH.slot CP.cp2 1 ComponentB.component unit absurd
    , HH.slot' CP.cp3 ComponentC.component unit absurd
    ]

In Halogen 5, all of this has been consolidated to a single row type where labels identify different child component types and the label's associated H.Slot value specifies the query, output, and slot type for the child component.

We can replace the ChildQuery and ChildSlot types with a single row type:

-- Halogen 5
type Slots =
  ( a :: H.Slot ComponentA.Query Void Unit
  , b :: H.Slot ComponentB.Query Void Int
  , c :: H.Slot ComponentC.Query Void Unit
  )

Instead of using ChildPath types (cp1, cp2, cp3, etc.) to identify components and slots, we now use symbol proxies for the labels in the row:

_a = SProxy :: SProxy "a"
_b = SProxy :: SProxy "b"
_c = SProxy :: SProxy "c"

render :: forall m. State -> H.ComponentHTML Action Slots m
render state =
  HH.div_
    [ HH.slot _a unit ComponentA.component unit absurd
    , HH.slot _b 1 ComponentB.component unit absurd
    , HH.slot _c unit ComponentC.component unit absurd
    ]

This may look similar on the surface to the prior non-row child query and child slot types, but in practice it is much nicer to deal with -- especially if you were one of the people out there who needed more than 10 types of child component, as we only provided helper types and premade ChildPath values up to that.

In Halogen 4 the slot, query, and queryAll had primed variants, slot', query', and queryAll', where the non-primed variants let you skip the ChildPath argument for components with only one type of child component.

In Halogen 5 there are only the un-primed variants. You must always provide an SProxy to the slot, query, and queryAll functions to identify the child component you are targeting.

The new row-based approach allows you greater flexibility to define helpers that work on slot types. For example, a common pattern in Halogen 5 applications is to define a Slot type synonym for a component in the same module in which the component is defined. This type synonym can specify the query and message types but leave the slot value unspecified, for a parent component to choose.

For example, if each of the ComponentA, ComponentB, and ComponentC modules in the example above had been defined with a type synonym for their slot type already:

module ComponentA where

type Slot = H.Slot Query Void

data Query = ...

component :: forall i o m. H.Component Query i Void m

Then parent components don't need to worry about specifying the query or message types for the child component:

type Slots =
  ( a :: ComponentA.Slot Unit
  , b :: ComponentB.Slot Int
  , c :: ComponentC.Slot Unit
  )

Subscriptions, Forking, and Event Sources

Halogen 5 introduces a number of ergonomic improvements to subscriptions, forking, and event sources, including a new EventSource API.

Subscriptions

The subscribe function in Halogen 5 now returns a SubscriptionId value that allows a subscription to be cancelled later with unsubscribe. Subscriptions could previously only be ended in response to an event -- the event source would close itself.

It's still possible for a subscription to unsubscribe itself. The subscribe' function passes the SubscriptionId into a function which returns the EventSource. That way the EventSource can raise an action with the relevant SubscriptionId.

Event Sources

Halogen 5 simplifies the EventSource API by introducing a new Emitter type and reducing the many, many variations of event source construction helpers to just affEventSource, effectEventSource, and eventListenerEventSource. Event sources now use queries instead of actions, and no longer require event handlers to return a subscription status.

Event sources have simpler types in Halogen 5:

-- Halogen 4
newtype EventSource f m =
  EventSource (m
    { producer :: CR.Producer (f SubscribeStatus) m Unit
    , done :: m Unit
    })

-- Halogen 5
newtype EventSource m a =
  EventSource (m
    { producer :: CR.Producer a m Unit
    , finalizer :: Finalizer m
    })

But it's not common to manually create an event source. Instead, you should use the new affEventSource and effectEventSource helper functions:

affEventSource
  :: forall m a
   . MonadAff m
  => (Emitter Aff a -> Aff (Finalizer Aff))
  -> EventSource m a

effectEventSource
  :: forall m a
   . MonadAff m
  => (Emitter Effect a -> Effect (Finalizer Effect))
  -> EventSource m a

These functions let you set up a new event source from a setup function. This setup function operates in Aff or Effect and allows you to emit actions to the current component (or close the event source) using the Emitter. The setup function returns a Finalizer to run when the event source is unsubscribed or the emitter is closed.

The emit function allows you to emit an action using the emitter provided by the affEventSource and effectEventSource functions. The close function lets you close the emitter and shut down the event source.

For example, this example creates an event source which will emit the Notify action after one second and then close the event source:

data Action = Notify String

myEventSource :: EventSource Aff Action
myEventSource = EventSource.affEventSource \emitter -> do
  Aff.delay (Milliseconds 1000.0)
  EventSource.emit emitter (Notify "hello")
  EventSource.close emitter
  pure mempty

There is also an eventListenerEventSource function which you can use to set up an event source that listens to events in the DOM.

eventListenerEventSource
  :: forall m a
   . MonadAff m
  => EventType
  -> EventTarget
  -> (Event -> Maybe a)
  -> EventSource m a

For example, we can subscribe to changes in the browser window width:

data Action = Initialize | Handler Window

handleAction = case _ of
  Initialize ->
    void $ H.subscribe do
      ES.eventListenerEventSource
        (EventType "resize")
        (Window.toEventTarget window)
        (Event.target >>> map (fromEventTarget >>> Handler))

  Handler window ->
    width <- liftEffect (innerWidth window)
    -- ...do something with the window width

When using event sources in components, you no longer need to respond to events with a SubscribeStatus:

-- Halogen 4
eval = case _ of
  HandleChange reply -> do
    -- ... your code
    pure (reply H.Listening)

-- Halogen 5
handleAction = case _ of
  HandleChange ->
    -- ... your code

Forks

In Halogen 4 the H.fork function returned a canceller function.

In Halogen 5 it returns a ForkId, which you can pass to the H.kill function to cancel the fork. This mirrors the H.subscribe function. Forks are now killed when a component is finalized, unless the fork occurred during finalization.

Performance Optimization with Lazy and Memoized

Halogen 5 introduces the ability to skip rendering for arbitrary HTML trees, not just at component boundaries as was the case in Halogen 4.

The new memoized function lets you skip rendering a tree of HTML given an equality predicate. If an argument is deemed equivalent to the value in the previous render then rendering and diffing will be skipped.

memoized
  :: forall a action slots m
   . (a -> a -> Boolean)
  -> (a -> ComponentHTML action slots m)
  -> a
  -> ComponentHTML action slots m

For example, you can skip rendering for equal state values by wrapping your component's render function:

myComponent = component
  { ...
  , render: memoized eq render
  , ...
  }

You can also skip rendering for referentially-equal arguments using the lazy, lazy2, and lazy3 functions. These work like memoized, but instead of taking an equality predicate they use referential equality.

Here's an example of skipping rendering a large list of items when the state it depends on is unchanged between renders:

-- Before
render state =
  HH.div_ [ generateItems state.totalItems ]

-- After
render state =
  HH.div_ [ HH.lazy generateItems state.totalItems ]

These functions are a convenient way to wring extra performance out of your render code.

Other Changes

Halogen 5 has also seen a number of other miscellaneous changes. These are quality of life improvements that don't affect many common workflows but which are worth noting.

Halt and HalogenM

The Halt constructor was removed from HalogenM. If a component needs to explode in that way, it should be done by lifting something into the component's m instead.

If Halt was being used for an infallible case in a higher order component eval, the same effect can be achieved now by returning Nothing.

If this doesn't mean anything to you, don't worry about it! Halting wasn't explained anywhere previously and was used internally for the most part.

DriverIO and App Disposal

The DriverIO type has been renamed to HalogenIO. You can now dispose of an entire Halogen app via the HalogenIO record returned from runUI. This will remove everything from the DOM and finalize the components. Attempting to query the DriverIO after this will return Nothing.

Updated Examples

The examples have been changed to try and best illustrate the feature they relate to, and just generally tidied up a bit. Some specifics:

  • The interpret example now works on a component that is using a ReaderT over Aff rather than a Free monad. ReaderT + Aff is a very common real world setup for an app's effect monad.
  • The higher-order-components example shows a expandable/collapsible container box kind of thing that allows interactions with the inner component when it is expanded.
  • The todo example has gone, as it was intended to show a fairly-but-not-entirely trivial example, but had weird conventions that nobody uses. @thomashoneyman's Real World Halogen is a much better and more comprehensive example of how an app might be structured and is up-to-date for Halogen 5.

File Inputs

The accept property (for file inputs) didn't have quite the right type before, it accepted a MediaType, but really should have allowed a collection of media types and file extensions. The type has been changed to a new InputAcceptType monoid to fix this.

Longer Type Variables in Type Signatures

The type variables have been renamed to full words in the component / query / etc. type signatures. Maybe this will help, maybe not - feedback is welcome and appreciated!

Migration to Spago

Spago has emerged as the preferred dependency manager and build tool for PureScript. Halogen 5 -- both the library and the examples -- is now migrated entirely to Spago, with Bower used solely for publication.