Deku logo

Core concepts

State

Learn how to use State Hooks in Deku.

In 2018, Sophie Alpert and Dan Abramov introduced React Hooks, a revolutionary way to manage stateful logic in component-based UI frameworks. Their mouths agape, the world of framework builders fell into three categories.

  1. Those who bemoaned having invented something hooks-like several years earlier only to see Facebook take the credit.
  2. Those who quickly rushed to implement hooks into their frameworks to ride on the coattail's of Facebook's success.
  3. Those who rebranded some feature of their framework as “hooks” so as not to miss out on the craze without needing to expend much effort.

Yours truly falls into all three categories, and it is with great pride that I introduce you to Deku's state-management paradigm à la React, Deku's State Hooks!


The state hook

Deku's state hooks fit comfortably on a single line and pack a lot of power. Let's see one now!

View on GithubVITE_START=TheStateHook pnpm example
module Examples.TheStateHook where

import Prelude
import Deku.Toplevel (runInBody)

import Data.Tuple.Nested ((/\))
import Deku.DOM.Attributes as DA
import Deku.Control (text, text_)
import Deku.DOM as D
import Deku.Do as Deku
import Deku.Hooks (useState)
import Deku.DOM.Listeners as DL
import Deku.Toplevel (runInBody)
import Effect (Effect)
import Effect.Random (random)
import ExampleAssitant (ExampleSignature)
import Deku.Toplevel (runInBody)

buttonClass =
  """inline-flex items-center rounded-md
border border-transparent bg-indigo-600 px-3 py-2
text-sm font-medium leading-4 text-white shadow-sm
hover:bg-indigo-700 focus:outline-none focus:ring-2
focus:ring-indigo-500 focus:ring-offset-2 mr-6""" :: String

main :: Effect Unit
main = do
  n <- random
  void $ runInBody Deku.do
    setNumber /\ number <- useState n
    D.div_
      [ D.button
          [ DA.klass_ buttonClass
          , DL.click_ \_ -> random >>= setNumber
          ]
          [ text_ "Update number" ]
      , text $ number <#>
          show >>> ("Here's a random number: " <> _)
      ]
Here's a random number: 0.5505149025421174

Deku.do

As previously mentioned, in PureScript and PureScript-inspired languages like Haskell, a do block is a way to write nested function calls as a sequence of instructions. This is why PureScript and its progeny are often collectively referred to as the best imperative language. Different types of instructions use different do blocks, and when you’re working with Deku hooks, that block is Deku.do.

Pushing to a hook

As we saw in the Components section, we can call an arbitrary effect, like raising an alert or writing to the console, from a listener like click. We can also push to a hook setter.

Let's have a look at the hook definition again.

setNumber /\ number <- useState n

Hook pushers (aka setters) are always on the left of the tuple returned from a hook. They always have the type a -> Effect Unit. In the example above a is a Number, but it could be a function, or an array, or really whatever you want to hook up to it.

Because pushers return something of type Effect Unit you can use them in any effectful context, including any Deku listener like DL.click or DL.input. In the example above, we generate a random number and use bind, aka >>= to push the result to setNumber.

DL.runOn DL.click $ random >>= setNumber

Using the hook in text

Let's look at our hook again, this time focusing on the right side.

setNumber /\ number <- useState n

The right side of our hook is of type Poll Number.

Raw values versus polls

If you’re coming from React, the fact that Deku hooks do not contain raw values like Number or String but rather Poll Number and Poll String will be a big change. Even though they’re not raw values, though, they can almost be used as such. In the Applicatives section we’ll learn how to do this.

We'll get to an exhaustive presentation of the Poll type later on, but for now, suffice it to say that it allows you to poll the DOM at any given moment in time to understand what, if anything, changed.

To use this Poll as DOM text, we’ll use the text function.text is like text_, but instead of taking a String, it accepts an argument of type Poll String. As our hook is a number, we have to map over our Poll Number to change it to an Poll String.

text $ number <#>
  show >>> ("Here's a random number: " <> _)

The symbol <#> maps over the Poll, turning its contents into some other type (in this case, String). At this point, it's worth mentioning that if operators like $, <#>, and /\ are unfamiliar to you, fear not! The PureScript documentation website Pursuit is your friend. You can search for all of these functions (and more) via the search bar.


Hooking into the DOM

In addition to animating text, hooks can be used to control DOM attributes and DOM elements as well.

Using a hook to control an attribute

Hooks can be used to control both attributes and listeners. In the following example, two different hooks are used to control two different attributes of the same anchor tag.

View on GithubVITE_START=HookInAnAttribute pnpm example
module Examples.HookInAnAttribute where

import Deku.Toplevel (runInBody)
import Effect (Effect)
import Prelude
import ExampleAssitant (ExampleSignature)
import Deku.Toplevel (runInBody)

import Data.String (Pattern(..), Replacement(..), replaceAll)
import Data.Tuple.Nested ((/\))
import Deku.DOM.Attributes as DA
import Deku.Control (text_)
import Deku.DOM as D
import Deku.Do as Deku
import Deku.Hooks (useState)
import Deku.DOM.Listeners as DL
import Deku.Toplevel (runInBody)

buttonClass :: String -> String
buttonClass color =
  replaceAll (Pattern "COLOR") (Replacement color)
    """ml-4 inline-flex items-center rounded-md
border border-transparent bg-COLOR-600 px-3 py-2
text-sm font-medium leading-4 text-white shadow-sm
hover:bg-COLOR-700 focus:outline-none focus:ring-2
focus:ring-COLOR-500 focus:ring-offset-2"""

main :: Effect Unit
main = void $ runInBody Deku.do
  setHrefSwitch /\ hrefSwitch <- useState false
  setStyleSwitch /\ styleSwitch <- useState false
  D.div_
    [ D.a
        [ DA.target_ "_blank"
        , DA.href $ hrefSwitch <#>
            if _ then "https://cia.gov" else "https://fbi.gov"
        , DA.style $ styleSwitch <#>
            if _ then "color:magenta;" else "color:teal;"
        ]
        [ text_ "Click me" ]
    , D.button
        [ DA.klass_ $ buttonClass "indigo"
        , DL.runOn DL.click $ hrefSwitch <#> not >>> setHrefSwitch
        ]
        [ text_ "Switch href" ]
    , D.button
        [ DA.klass_ $ buttonClass "green"
        , DL.runOn DL.click $ styleSwitch <#> not >>> setStyleSwitch
        ]
        [ text_ "Switch style" ]
    ]
Click me

In addition to controlling attributes of the anchor, these hooks also control the click listeners. That is, each hook's value is stored in a listener, and whenever the hook updates, its new value is used to create a new listener that replaces the old one.

Unsetting an attribute with a hook

Sometimes, you need to unset an attribute. You can do that by using the DL.unset combinator.

View on GithubVITE_START=UnsettingAttributes pnpm example
module Examples.UnsettingAttributes where

import Prelude
import Deku.Toplevel (runInBody)

import Data.Filterable (filter)
import Data.String (Pattern(..), Replacement(..), replaceAll)
import Data.Tuple.Nested ((/\))
import Deku.Control (text_)
import Deku.DOM as D
import Deku.DOM.Attributes as DA
import Deku.DOM.Listeners as DL
import Deku.Do as Deku
import Deku.Hooks (useState)
import Deku.Toplevel (runInBody)
import Effect (Effect)
import ExampleAssitant (ExampleSignature)
import Deku.Toplevel (runInBody)

buttonClass :: String -> String
buttonClass color =
  replaceAll (Pattern "COLOR") (Replacement color)
    """ml-4 inline-flex items-center rounded-md
border border-transparent bg-COLOR-600 px-3 py-2
text-sm font-medium leading-4 text-white shadow-sm
hover:bg-COLOR-700 focus:outline-none focus:ring-2
focus:ring-COLOR-500 focus:ring-offset-2"""

main :: Effect Unit
main = void $ runInBody Deku.do
  setStyleSwitch /\ styleSwitch <- useState false
  D.div_
    [ D.a
        [ DA.target_ "_blank"
        , DA.style $ filter identity styleSwitch $> "color:magenta;"
        , DA.unset @"style" $ filter not styleSwitch
        ]
        [ text_ "Click me" ]
    , D.button
        [ DA.klass_ $ buttonClass "pink"
        , DL.runOn DL.click $ styleSwitch <#> not >>> setStyleSwitch
        ]
        [ text_ "Switch style" ]
    ]
Click me

Under the hood, unsetting an attribute calls the DOM's removeAttribute function, so it'll be as if the attribute were never there. What attribute? Exactly…

Using a hook to switch between elements

You can also use a hook to switch between elements. This is particularly useful for tabbed navigation. In the example below, a hook is used to switch between an image, a video, and an SVG. This is accomplished via the <#~> operator.

View on GithubVITE_START=SwitchBetweenElements pnpm example
module Examples.SwitchBetweenElements where

import Prelude
import Deku.Toplevel (runInBody)

import Data.String (Pattern(..), Replacement(..), replaceAll)
import Data.Tuple.Nested ((/\))
import Deku.Control (text_)
import Deku.DOM.SVG as DS
import Deku.DOM.SVG.Attributes as DSA
import Deku.DOM as D
import Deku.DOM.Attributes as DA
import Deku.DOM.Listeners as DL
import Deku.Do as Deku
import Deku.Hooks (useState, (<#~>))
import Deku.Toplevel (runInBody)
import Effect (Effect)
import ExampleAssitant (ExampleSignature)
import Deku.Toplevel (runInBody)

buttonClass :: String -> String
buttonClass color =
  replaceAll (Pattern "COLOR") (Replacement color)
    """inline-flex items-center rounded-md
border border-transparent bg-COLOR-600 px-3 py-2 mx-1
text-sm font-medium leading-4 text-white shadow-sm
hover:bg-COLOR-700 focus:outline-none focus:ring-2
focus:ring-COLOR-500 focus:ring-offset-2"""

data Element = Image | Video | SVG

bunny :: String
bunny =
  "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"

pic :: String
pic = "https://picsum.photos/150"

main :: Effect Unit
main = void $ runInBody Deku.do
  setImageType /\ imageType <- useState Image
  D.div_
    [ D.div_
        [ imageType <#~>
            case _ of
              Image -> D.img [ DA.alt_ "Some lorem picsum", DA.src_ pic ]
                []
              Video -> D.video
                [ DA.controls_ "controls"
                , DA.preload_ "none"
                , DA.width_ "250"
                , DA.height_ "250"
                , DA.autoplay_ "true"
                ]
                [ D.source
                    [ DA.src_ bunny
                    , DA.xtype_ "video/webm"
                    ]
                    []
                ]
              SVG -> DS.svg
                [ DSA.height_ "170"
                , DSA.width_ "170"
                ]
                [ DS.circle
                    [ DSA.cx_ "75"
                    , DSA.cy_ "75"
                    , DSA.r_ "70"
                    , DSA.stroke_ "black"
                    , DSA.strokeWidth_ "3"
                    , DSA.fill_ "red"
                    ]
                    []
                ]
        ]
    , D.div_
        [ D.button
            [ DA.klass_ $ buttonClass "amber"
            , DL.click_ \_ -> setImageType Image
            ]
            [ text_ "Image" ]
        , D.button
            [ DA.klass_ $ buttonClass "cyan"
            , DL.click_ \_ -> setImageType Video
            ]
            [ text_ "Video" ]
        , D.button
            [ DA.klass_ $ buttonClass "green"
            , DL.click_ \_ -> setImageType SVG
            ]
            [ text_ "SVG" ]
        ]
    ]
Some lorem picsum

If your DOM is mostly static and has a few switching elements within it, consider using multiple <#~> operators instead of one global <#~>, as it will generally result in better performance.

Using a hook to control presence

You can also use a boolean hook to control the presence or absence of an object via the guard function.

View on GithubVITE_START=HookControlsPresence pnpm example
module Examples.HookControlsPresence where

import Deku.Toplevel (runInBody)
import Effect (Effect)
import Prelude
import ExampleAssitant (ExampleSignature)
import Deku.Toplevel (runInBody)

import Data.Tuple.Nested ((/\))
import Deku.Control (text, text_)
import Deku.DOM as D
import Deku.Do as Deku
import Deku.Hooks (useState, guard)
import Deku.DOM.Attributes as DA
import Deku.Toplevel (runInBody)

import Deku.DOM.Listeners as DL
import Deku.Toplevel (runInBody)

main :: Effect Unit
main = void $ runInBody Deku.do
  setPresence /\ presence <- useState true
  D.div_
    [ guard presence (text_ "Now you see me, ")
    , D.a
        [ DA.klass_ "cursor-pointer"
        , DL.runOn DL.click $ presence <#> not >>> setPresence
        ]
        [ text $ presence <#>
            if _ then "now you don't." else "Oops, come back!"
        ]
    ]
Now you see me, now you don't.

In case you ever want to typeset an empty element (meaning an element that does not appear in the DOM at all), you can use blank. In fact, the definition of guard above is just the following.

guard eb d = eb <#~> if _ then d else mempty

State without initial values

It's possible to have a state hook without an initial value. In that case, whatever the hook was controlling, be it text, an attribute, or an element, is simply omitted from the DOM.

Empty until full

Let's revisit the first example from this section, using an uninitialized state.

View on GithubVITE_START=EmptyUntilFull pnpm example
module Examples.EmptyUntilFull where

import Prelude
import Deku.Toplevel (runInBody)

import Data.Tuple.Nested ((/\))
import Deku.DOM.Attributes as DA
import Deku.Control (text, text_)
import Deku.DOM as D
import Deku.Do as Deku
import Deku.Hooks (useState')
import Deku.DOM.Listeners as DL
import Deku.Toplevel (runInBody)
import Effect (Effect)
import Effect.Random (random)
import ExampleAssitant (ExampleSignature)
import Deku.Toplevel (runInBody)

buttonClass =
  """inline-flex items-center rounded-md
border border-transparent bg-indigo-600 px-3 py-2
text-sm font-medium leading-4 text-white shadow-sm
hover:bg-indigo-700 focus:outline-none focus:ring-2
focus:ring-indigo-500 focus:ring-offset-2 mr-6""" :: String

main :: Effect Unit
main = void $ runInBody Deku.do
  setNumber /\ number <- useState'
  D.div_
    [ D.button
        [ DA.klass_ buttonClass
        , DL.click_ \_ -> random >>= setNumber
        ]
        [ text_ "Update number" ]
    , text $ number <#>
        show >>> ("Here's a random number: " <> _)
    ]

The only difference with the initial example is that the text element is rendered to the DOM after the first value has been provided.

The useHot hook

It's important to know that the hooks above are not memoized, meaning that they do not store their most recent value. They simply pass through whatever comes down the pipe. This comes from Deku's tradition as an engine for games and interactive art, where we need to compose together streams of data. However, in certain cases, like when we’re polling a user profile, you always want to use the most recent value.

To see this in practice, in the snippet below, press button A a few times and then press B once and only once (even if you don't think it's responding). Then press A again a few times. What do you think will happen?

View on GithubVITE_START=ANoteOnMemoization pnpm example
module Examples.ANoteOnMemoization where

import Prelude
import Deku.Toplevel (runInBody)

import Control.Alt ((<|>))
import Data.String (replaceAll, Pattern(..), Replacement(..))
import Data.Tuple.Nested ((/\))
import Deku.DOM.Attributes as DA
import Deku.Control (text, text_)
import Deku.DOM as D
import Deku.Do as Deku
import Deku.Hooks (guard, useState, useState')
import Deku.DOM.Listeners as DL
import Deku.Toplevel (runInBody)
import Effect (Effect)
import Effect.Random (random)
import ExampleAssitant (ExampleSignature)
import Deku.Toplevel (runInBody)

buttonClass :: String -> String
buttonClass color =
  replaceAll (Pattern "COLOR") (Replacement color)
    """ml-4 inline-flex items-center rounded-md
border border-transparent bg-COLOR-600 px-3 py-2
text-sm font-medium leading-4 text-white shadow-sm
hover:bg-COLOR-700 focus:outline-none focus:ring-2
focus:ring-COLOR-500 focus:ring-offset-2"""

main :: Effect Unit
main = do
  n <- random
  void $ runInBody Deku.do
    setNumber /\ number <- useState'
    setPresence /\ presence <- useState false
    D.div_
      [ D.div_
          [ text $ (pure n <|> number) <#> show >>>
              ("Here's a random number: " <> _)
          ]
      , D.div_
          [ D.button
              [ DA.klass_ $ buttonClass "pink"
              , DL.click_ \_ -> random >>= setNumber
              ]
              [ text_ "A" ]
          , D.button
              [ DA.klass_ $ buttonClass "green"
              , DL.runOn DL.click $ presence <#> not >>> setPresence
              ]
              [ text_ "B" ]
          ]
      , D.div_
          [ guard presence
              $ text
              $ number <#> show >>>
                  ("Here's the same random number: " <> _)
          ]
      ]
Here's a random number: 0.6644683430009108

Because the hook simply passes through values as it receives them, as there was no simultaneous value coming from A when B was pressed, the guarded section didn't activate until A was pressed again. In effect, while the hook had an initial value n for the first textcomponent, it lacked an initial value for any component that was created afterwards. You can think of initial values to hooks as being relevant only at the moment of creation.

It is indeed possible to have hooks that always supply their most recent value, but it requires using a hook we haven't learned about yet: useHot. Fear not though, an example will thusly be presented.

View on GithubVITE_START=AWayToMemoize pnpm example
module Examples.AWayToMemoize where

import Prelude
import Deku.Toplevel (runInBody)

import Data.String (replaceAll, Pattern(..), Replacement(..))
import Data.Tuple.Nested ((/\))
import Deku.DOM.Attributes as DA
import Deku.Control (text, text_)
import Deku.DOM as D
import Deku.Do as Deku
import Deku.Hooks (guard, useHot, useState)
import Deku.DOM.Listeners as DL
import Deku.Toplevel (runInBody)
import Effect (Effect)
import Effect.Random (random)
import ExampleAssitant (ExampleSignature)
import Deku.Toplevel (runInBody)

buttonClass :: String -> String
buttonClass color =
  replaceAll (Pattern "COLOR") (Replacement color)
    """ml-4 inline-flex items-center rounded-md
border border-transparent bg-COLOR-600 px-3 py-2
text-sm font-medium leading-4 text-white shadow-sm
hover:bg-COLOR-700 focus:outline-none focus:ring-2
focus:ring-COLOR-500 focus:ring-offset-2"""

main :: Effect Unit
main = do
  n <- random
  void $ runInBody Deku.do
    setNumber /\ number <- useHot n
    setPresence /\ presence <- useState false
    D.div_
      [ D.div_
          [ text $ number <#> show >>>
              ("Here's a random number: " <> _)
          ]
      , D.div_
          [ D.button
              [ DA.klass_ $ buttonClass "pink"
              , DL.click_ \_ -> random >>= setNumber
              ]
              [ text_ "A" ]
          , D.button
              [ DA.klass_ $ buttonClass "green"
              , DL.runOn DL.click $ presence <#> not >>> setPresence
              ]
              [ text_ "B" ]
          ]
      , D.div_
          [ guard presence
              $ text
              $ number <#> show >>>
                  ("Here's the same random number: " <> _)
          ]
      ]
Here's a random number: 0.8147566273835538

useHot is like useState except instead of replaying the initial value, it replays the most recent value.