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.
- Those who bemoaned having invented something hooks-like several years earlier only to see Facebook take the credit.
- Those who quickly rushed to implement hooks into their frameworks to ride on the coattail's of Facebook's success.
- 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!
VITE_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
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
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.
VITE_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" ]
]
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.
VITE_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" ]
]
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.
VITE_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" ]
]
]
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.
VITE_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.
VITE_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?
VITE_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 text
component, 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.
VITE_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.