Deku logo

Core concepts

Effects

A lesson on inverting control, or "noctlor" (that's control inverted).

If you’re familiar with React or Halogen, the word effect is often used to refer to one of two related concepts:

In both frameworks, the mere presence or absence of a component can trigger all sorts of side effects like REST API calls or robot arms moving. Deku offers no such accoutrements (sorry, robots). Instead, side effects are completely initiated by a component's enclosing scope.

Consider the following typical flow in a garden-variety web app:

  • Someone clicks on a button, revealing a component.
  • This component has some effectful initialization code.
  • When the component goes off the page, it also has some effectful teardown code.

In Deku, those effects are the responsibility of the parent component. If there's no parent, it's the responsibility of the app's initialization code.

This section will explore Deku's effect philosophy and also present some strategies to use when you just can't shake those old React/Halogen effectful habits (we've all been there, you’re not alone).


Hydration

In Deku, components traditionally manage side effects by hydrating their children. Let's look at that what that means and how to implement it in practice.

An alternative effect model

First, a note on why Deku ❤️ hydration. Deku was originally developed to build games and musical instruments. Because of that, the contributors developed a culture of components being as “dumb” as possible. This means that, when a component mounts or unmounts from an application, the act of appearing or disappearing should have minimal impact on the UI.

Why the austerity? Because if you’re writing a game and aiming for 60 FPS, you can't mess around. Random side-effects here and there add up and tank performance. If you’re reading this documentation, it's likely because you too are obsessively anti-jank. Deku wages this crusade against jank at every level of a project, and the docs below will give you helpful hints on how to do it.

Injecting dependencies

Let's explore the Deku-ian effect model with a concrete example. Imagine that you are building an app called Image Roulette that reports when a user is watching or not watching an image. Whenever an image comes into focus, you need to ping the backend that it is being viewed, and whenever an image goes offscreen, you need to report that it is no longer being viewed.

In React, we would likely make a component called SmartImage that reports its own presence or absence via a REST API call on mount and dismount.

In the following application, we’ll implement a miniature version of Image Roulette with dummy functions for the following API calls:

  • fetchNewRandomImage: Fetch a new random image URL and increase the watcher count.
  • decreaseImageWatchCount: Indicate we are no longer looking at the image.
View on GithubVITE_START=InjectingDependencies pnpm example
module Examples.InjectingDependencies where

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

import Data.Int (floor)
import Data.JSDate (getTime, now)
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.Pursx (pursx)
import Deku.Toplevel (runInBody)

import Effect.Aff (Milliseconds(..), delay, launchAff_)
import Effect.Class (liftEffect)
import Effect.Random (random)
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

data UIState
  = Beginning
  | Loading
  | Image { url :: String, watcherCount :: Int }

main :: Effect Unit
main = void $ runInBody Deku.do
  let
    fetchNewRandomImage = do
      delay (Milliseconds 300.0)
      n <- liftEffect (getTime <$> now)
      c <- liftEffect random
      pure $
        { url: "https://picsum.photos/seed/" <> show n <> "/200"
        , watcherCount: floor (c * 4200.0)
        }
    decreaseImageWatchCount _ = do
      delay (Milliseconds 300.0)
      c <- liftEffect random
      pure $ { watcherCount: floor (c * 4200.0) }
  setUIState /\ uiState <- useState Beginning
  D.div_
    [ D.button
        [ DA.klass_ buttonClass
        , let
            fetcher = do
              newRandomImage <- fetchNewRandomImage
              liftEffect $ setUIState $ Image newRandomImage
            loader = liftEffect $ setUIState Loading
          in
            DL.runOn DL.click $ uiState <#> case _ of
              Beginning -> do
                launchAff_ do
                  loader
                  fetcher
              Loading -> pure unit
              Image { url } ->
                launchAff_ do
                  loader
                  _ <- decreaseImageWatchCount url
                  fetcher
        ]
        [ text $ uiState <#> case _ of
            Beginning -> "Get Image"
            _ -> "Change Image"
        ]
    , D.div_
        [ uiState <#~> case _ of
            Beginning -> mempty
            Image { url, watcherCount } -> D.div_
              [ D.img [ DA.alt_ "Some lorem picsum", DA.src_ url ] []
              , D.div_
                  [ text_ $
                      "Watcher count (including you): " <> show
                        watcherCount
                  ]
              ]
            Loading ->
              D.div [ DA.klass_ "p-10" ]
                [ pursx @Loading {} ]
        ]
    ]

type Loading =
  """<div role="status">
    <svg aria-hidden="true" class="mr-2 w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
        <path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="#7393B3"/>
        <path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/>
    </svg>
    <span class="sr-only">Loading...</span>
</div>"""

The important thing to retain from this example is that the entire effect lifecycle controlled in the button's click listener. In addition to extricating effectful logic from the image presentation components, this has the added advantage of being able to throttle requests during loading.


Common effectful flows

This section explores common effectful flows, like random number generators and calls to REST APIs, and how to achieve them by triggering these effects from outer scopes.

A time-stamper

In the example below, we mint a very-effectful fresh timestamp every time the text Current timestamp is clicked. The same pattern is accomplishable via useRef. As is the case with many things in Deku, there is more than one way to skin a Gerudian Lizalfos.

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

import Prelude
import Deku.Toplevel (runInBody)

import Data.JSDate (getTime, now)
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 ExampleAssitant (ExampleSignature)
import Deku.Toplevel (runInBody)

main :: Effect Unit
main = void $ runInBody Deku.do
  setCurrentTime /\ currentTime <- useState'
  D.div_
    [ D.a
        [ DL.click_ \_ -> (getTime <$> now >>= setCurrentTime)
        , DA.klass_ "cursor-pointer"
        ]
        [ text_ "Current timestamp" ]
    , text_ ": "
    , text (show <$> currentTime)
    ]

Making API calls

In PureScript, asynchronous effects can be triggered by using launchAff in an effectful context like a click listener. This allows us to use the same flow from the timestamp example above and adapt it to asynchronous code.

View on GithubVITE_START=RunningAffsInResponseToAnEvent pnpm example
module Examples.RunningAffsInResponseToAnEvent 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.Aff (launchAff_)
import Effect.Class (liftEffect)
import ExampleAssitant (ExampleSignature)
import Fetch (Method(..), fetch)
import Deku.Toplevel (runInBody)

main :: Effect Unit
main = void $ runInBody Deku.do
  setResponse /\ response <- useState'
  D.div_
    [ D.a
        [ DL.click_ \_ -> launchAff_ do
            { text: t } <- fetch "https://httpbin.org/post"
              { method: POST
              , body: """{"hello":"world"}"""
              , headers: { "Content-Type": "application/json" }
              }
            t' <- t
            liftEffect $ setResponse t'
        , DA.klass_ "cursor-pointer"
        ]
        [ text_ "Click for a random http response" ]
    , text_ ": "
    , text (show <$> response)
    ]

Waiting for API calls to resolve

In the preivous section, we saw how to launch a one-off effect using launchAff_. Sometimes, though, we want our ansychronous code to execute in a certain order. For this, there is launchAff, which returns a Fiber. These are sort of like JS promises - they are memoized on completion and, when joined in an Aff, will block until completion. You can use them to run asynchronous code in series.

The following example will emit an aff on each event and the affs will execute one after the other. It does no cancellation, so if affs pile up, they will keep going until the element leaves the screen. If you’re a glutton for punishment, click the link really fast ~20 times while watching your network tab.

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

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

import Data.Tuple.Nested ((/\))
import Deku.DOM.Attributes as DA
import Deku.Toplevel (runInBody)

import Deku.Control (text, text_)
import Deku.DOM as D
import Deku.Do as Deku
import Deku.Hooks (useState, useState')
import Deku.DOM.Listeners as DL
import Deku.Toplevel (runInBody)

import Effect.Aff (joinFiber, launchAff, try)
import Effect.Class (liftEffect)
import Fetch (Method(..), fetch)
import Deku.Toplevel (runInBody)

main :: Effect Unit
main = void $ runInBody Deku.do
  setResponse /\ response <- useState'
  setFiber /\ fiber <- useState (pure unit)
  D.div_
    [ D.a
        [ DL.runOn DL.click $ fiber <#> \f -> setFiber =<< launchAff do
            _ <- try $ joinFiber f
            { text: t } <- fetch "https://httpbin.org/post"
              { method: POST
              , body: """{"hello":"world"}"""
              , headers: { "Content-Type": "application/json" }
              }
            t' <- t
            liftEffect $ setResponse t'

        , DA.klass_ "cursor-pointer"
        ]
        [ text_ "Click for a random http response" ]
    , text_ ": "
    , text (show <$> response)
    ]

Affable asynchronicity in PureScript

Up until now, the side-effects we have seen have all been synchronous. In PureScript, we also have Aff for asynchronous effects. You can think of them as Promises that are meant to be broken (see Canceler). liftEffect converts an Effectful computation to an Affish (Affular? Afferific?) computation and launchAff_ takes an Aff and launches it from within an Effect, making that block run asynchronously.

Canceling stale API calls

This variation of the code above does cancellation via the macabre killFiber, so when a new aff comes down the pipe, the previous one is cancelled.

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

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

import Data.Tuple.Nested ((/\))
import Deku.DOM.Attributes as DA
import Deku.Toplevel (runInBody)

import Deku.Control (text, text_)
import Deku.DOM as D
import Deku.Do as Deku
import Deku.Hooks (useState, useState')
import Deku.DOM.Listeners as DL
import Deku.Toplevel (runInBody)

import Effect.Aff (error, killFiber, launchAff)
import Effect.Class (liftEffect)
import Fetch (Method(..), fetch)
import Deku.Toplevel (runInBody)

main :: Effect Unit
main = void $ runInBody Deku.do
  setResponse /\ response <- useState'
  setFiber /\ fiber <- useState (pure unit)
  D.div_
    [ D.a
        [ DL.runOn DL.click $ fiber <#> \f -> setFiber =<< launchAff do
            killFiber (error "Who needed that request?") f
            { text: t } <- fetch "https://httpbin.org/post"
              { method: POST
              , body: """{"hello":"world"}"""
              , headers: { "Content-Type": "application/json" }
              }
            t' <- t
            liftEffect $ setResponse t'

        , DA.klass_ "cursor-pointer"
        ]
        [ text_ "Click for a random http response" ]
    , text_ ": "
    , text (show <$> response)
    ]

For game developers

If you’re building a game with Deku, you’re likely used to a different effect model than than the webapp-y one described above. Engines like Unity and Unreal provide two basic functions for GameObjects (Unity) or Actors (Unreal). Using Unreal's terminology, these two functions are:

  • BeginPlay: called when play begins for an actor.
  • Tick: called on each tick of the game loop.

BeginPlay is of a different ilk than the casual(-ly cruel) one-off effects run on components' mounting and dismounting à la React. It's integral to a game framework's logic. In functional programming, we call this an effect system. That is, it's a predictable contract vis-à-vis side effects that's managed by some interpreter or engine.

We've already seen a Deku-ian effect system in the Providers section. Providers are a category of effects. The specific effect is that of providing information to an otherwise pure component. In this section, we’ll expand upon that notion to create an effect system with a BeginPlay function. In anger, Deku games often use this strategy.

Creating our effect system

Let's create a little game where a bunch of svg sprites move across the screen. Click them before they turn red to score points! If they turn red, you lose a point. Try not to dip below 0!

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

import Prelude
import Deku.Toplevel (runInBody)

import Control.Alt ((<|>))
import Control.Monad.ST.Class (liftST)
import Control.Monad.ST.Ref (new, read, write)
import Data.Array (drop, take, dropEnd, zipWith)
import Data.Array as Array
import Data.Compactable (compact)
import Data.DateTime.Instant (Instant, unInstant)
import Data.Either (Either(..), either)
import Data.Foldable (sum)
import Data.Int (round, toNumber)
import Data.Maybe (Maybe(..), isJust, maybe)
import Data.Newtype (unwrap)
import Data.Number (floor, sign)
import Data.Number.Format (fixed, toStringWith)
import Data.Op (Op(..))
import Data.Tuple (Tuple(..))
import Data.Tuple.Nested ((/\))
import Deku.Control (text, text_)
import Deku.Core (Nut, useRant)
import Deku.DOM as D
import Deku.DOM.Attributes as DA
import Deku.DOM.Listeners as DL
import Deku.DOM.SVG as DS
import Deku.DOM.SVG.Attributes as DSA
import Deku.Do as Deku
import Deku.Hooks (guardWith, useDynAtEnd, useHot, useState)
import Deku.Toplevel (runInBody)
import Effect (Effect)
import Effect.Random (randomBool, randomRange)
import Effect.Timer (clearTimeout)
import ExampleAssitant (ExampleSignature)
import FRP.Event (Event, fold, mapAccum, sampleOnRight)
import FRP.Event.AnimationFrame (animationFrame')
import FRP.Event.Time (interval', withDelay, withTime)
import FRP.Poll (sham)
import Record (union)
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

type SpriteInit' =
  ( positionX :: Number
  , positionY :: Number
  , diameter :: Number
  , velocityX :: Number
  , velocityY :: Number
  , startingTime :: Number
  , lifespan :: Number
  , bounds :: Number
  )

type SpriteInit =
  { canceller :: Effect Unit
  | SpriteInit'
  }

type SpriteEnv = { time :: Number }

type SpriteInfo =
  { bounds :: Number
  , positionX :: Number
  , positionY :: Number
  , dying :: Maybe (Either Number Number)
  , dead :: Maybe Number
  , isClicked :: Boolean
  , diameter :: Number
  , velocityX :: Number
  , velocityY :: Number
  , startingTime :: Number
  , lifespan :: Number
  , time :: Number
  }

withSpriteDelay
  :: Effect Unit
  -> Op (Effect Unit) SpriteInit
  -> Op (Effect Unit) { | SpriteInit' }
withSpriteDelay scoreDown (Op f) = Op \i -> do
  -- change
  let
    Op wd = withDelay (round (i.lifespan * 1000.0)) $ Op case _ of
      Left tid -> f (i `union` { canceller: clearTimeout tid })
      Right _ -> scoreDown
  wd unit

withSpriteInit
  :: Number -> Op (Effect Unit) { | SpriteInit' } -> Op (Effect Unit) Instant
withSpriteInit bounds (Op f) = Op \i -> do
  diameter <- randomRange (min bounds 40.0) (min bounds 100.0)
  lifespan <- randomRange 3.0 9.0
  let radius = diameter / 2.0
  let
    randPos = randomRange (radius + 1.0)
      (bounds - radius - 1.0)
  positionX <- randPos
  positionY <- randPos
  let
    randVel = mul <$> randomRange 4.0 40.0 <*>
      ((if _ then 1.0 else -1.0) <$> randomBool)
  velocityX <- randVel
  velocityY <- randVel
  let startingTime = (unwrap $ unInstant i) / 1000.0
  f
    { positionX
    , positionY
    , diameter
    , velocityX
    , velocityY
    , startingTime
    , lifespan
    , bounds
    }

beginPlay :: SpriteInit -> SpriteInfo
beginPlay
  { positionX
  , positionY
  , diameter
  , velocityX
  , velocityY
  , startingTime
  , lifespan
  , bounds
  } = do
  let
    time = startingTime
  { positionX
  , positionY
  , isClicked: false
  , dying: Nothing
  , dead: Nothing
  , diameter
  , velocityX
  , velocityY
  , time
  , startingTime: time
  , lifespan
  , bounds
  }

logisticMap :: Number -> Number
logisticMap x = 3.99 * x * (1.0 - x)

tick :: SpriteEnv -> SpriteInfo -> SpriteInfo
tick env i' = do
  let
    i = i' { time = env.time }
    wither t = do
      if env.time - t > 0.5 then do
        i { dead = Just env.time, diameter = 0.0 }
      else
        i { diameter = max 0.0 (i.diameter * (1.0 - ((env.time - t) * 2.0))) }
  if isJust i.dead then i
  else case i.dying of
    Just (Left x) -> wither x
    Just (Right x) -> wither x
    _ -> do
      if i.isClicked then i { dying = Just (Right env.time) }
      else do
        let dTime = env.time - i'.time
        let span = env.time - i.startingTime
        if span > i.lifespan then do
          i { dying = Just $ Left env.time }
        else do
          let
            posAndVel p v r = do
              let np = dTime * v + p
              if np < 0.0 || np > i.bounds then do
                let nv = (-1.0 * sign np) * r * 36.0 + 4.0
                posAndVel p nv (logisticMap r)
              else
                Tuple np v
            rx = env.time - floor env.time
            ry = 1.0 - rx
            Tuple positionX velocityX = posAndVel i.positionX
              i.velocityX
              rx
            Tuple positionY velocityY = posAndVel i.positionY
              i.velocityY
              ry
          i
            { positionX = positionX
            , positionY = positionY
            , velocityX = velocityX
            , velocityY = velocityY
            }

mean :: Array Number -> Number
mean ar =
  let
    alen = Array.length ar
  in
    if alen == 0 then 0.0
    else (sum ar) / (toNumber alen)

meanDiff :: Array Number -> Number
meanDiff ar = mean $ zipWith (\a b -> 1.0 / ((a - b) / 1000.0)) (dropEnd 1 ar)
  (drop 1 ar)

main :: Effect Unit
main = do
  let bounds = 1000
  unsub <- liftST $ new (pure unit)
  af <- animationFrame' withTime
  void $ runInBody Deku.do
    setGameStarted /\ gameStarted <- useHot Nothing
    setScoreBudge /\ scoreBudge <- useState $ Nothing
    let
      score = fold (\a b -> maybe 0 ((if _ then 1 else (-1)) >>> add a) b)
        0
        scoreBudge
    D.div_
      [ D.div_
          [ D.button
              [ DA.klass_ buttonClass
              , DL.runOn DL.click $ gameStarted <#> \s -> do
                  if isJust s then do
                    join (liftST $ read unsub)
                    void $ liftST $ write (pure unit) unsub
                    setGameStarted Nothing
                    setScoreBudge Nothing
                  else do
                    i <- interval'
                      ( withSpriteDelay (setScoreBudge $ Just false) >>>
                          (withSpriteInit (toNumber bounds))
                      )
                      400
                    void $ liftST $ write i.unsubscribe unsub
                    setGameStarted (Just i.event)
              ]
              [ text $ gameStarted <#> isJust >>>
                  if _ then "Quit" else "Start Game"
              ]
          , guardWith gameStarted \_ -> do
              D.div_
                [ text_ "Score: "
                , text $ show <$> score
                , text_ " FPS: "
                , text $ toStringWith (fixed 2) <$>
                    ( sham $ mapAccum
                        (\a b -> Tuple (take 10 ([ b ] <> a)) (meanDiff a))
                        []
                        (map (_.time >>> unInstant >>> unwrap) af.event)
                    )
                ]
          ]
      , guardWith gameStarted \emitter -> DS.svg
          [ DSA.viewBox_ $ "0 0 " <> show bounds <> " " <> show bounds
          , DSA.width_ "400"
          , DSA.height_ "400"
          ]
          [ Deku.do
              { value } <- useDynAtEnd
                ( sham emitter <#> makeFreshSprite (setScoreBudge (Just true))
                    (af.event <#> _.time)
                )
              value
          ]
      ]
  where

  makeFreshSprite
    :: Effect Unit
    -> Event Instant
    -> SpriteInit
    -> Nut
  makeFreshSprite scoreUp animate si = Deku.do
    setClicked /\ clicked <- useState false
    let bp = beginPlay si
    ticked <- useRant $
      mapAccum
        (\a (Tuple time clk) -> Tuple (tick { time } (a { isClicked = clk })) a)
        bp
        ( sampleOnRight clicked
            (Tuple <$> (sham animate <#> unInstant >>> unwrap >>> (_ / 1000.0)))
        )
    DS.circle
      [ DL.click_ \_ -> do
          setClicked true
          si.canceller
          scoreUp
      , DSA.cx $ _.positionX >>> show <$> ticked
      , DSA.cy $ _.positionY >>> show <$> ticked
      , DSA.r $ _.diameter >>> (_ / 2.0) >>> show <$> ticked
      , DSA.klass
          ( pure "fill-blue-500" <|>
              ( map (either (const "fill-red-500") (const "fill-green-500"))
                  $ compact
                  $ (_.dying <$> ticked)
              )
          )
      ]
      []

That example is a bit heftier than the other ones, clocking in at around 300 lines of code. But we've created a full-fledged game engine, so that's not that bad. Let's unpack what's going on.

  • Just like Unreal, we have beginPlay and tick methods for our Actors.
  • We use the combinator pattern viawithX à la hyrule, the FRP package on which Deku is based, to isolate the Effects.
  • We pile up our sprites as Nuts via a useDynAtEnd. The actors, at the end of the day, are just dynamic elements in a Deku app.

Even more improtantly, it cruises at 60fps and, on my Mac running on battery power with too many tabs open, barely eats up any of the scripting in the rendering loop, leaving ample headroom for experience (clicking around) and whatever else you wanna add to the game.

A JS performance profile from Google Chrome

Bravo!

Take a moment to congratulate yourself for how far you've come. Then, take two moments to congratulate me on toiling over a hot computer for your edification. We've gone from some humble beginnings in Hello world to a full-fledged game!

At the end of the day, Deku is all about making games and musical instruments. Its design tradeoffs favor keeping your game at 60fps over whatever its blind spots are. It does this by eschewing the VDOM model in favor of something more reactive and snappy. At the same time, abstractions like hooks that originated in the VDOM world are easier to grok than reactive programming. When possible, the framework offers these abstractions to its devotees.

Now that you've meticulously poured over and internalized all of the text and examples in this section, you’re pretty far down the FRP rabbit hole. As the light of day recedes, you see that a cranny that may give way to a whole new chapter of this adventure. Against all reason, and certainly against the objections of your colleagues, friends, and family, you decide to keep going.

In the next section, you’re going to learn about Functional Reactive Programming. It will elevate you from Deku Grand Master to an echelon of Deku prowess that is incomprehensible for those who have not attained it. Along the way, you will bend space-time, defy gravity, quantum-entangle and solve a Saturday NYT crossword puzzle. Read on!