Deku logo

Core concepts

Portals

Dealing with stateful DOM components.

You've read the Deku docs, you’re itching to create your first game, and you decide to make a game that involves moving a video sprite from tile to tile. Nice!

There's one issue, though. Can you spot it? Look closely at the video as it moves from tile to tile. Each time you move it, the video restarts! But we don't want that, we want the video to be continuous as it's jumping from place to place.

In games and other multimedia sites, it's common to use video, audio, and canvas elements that are stateful. These elements often stay put in the DOM, so we don't need to manage their statefulness. But in cases where we need to move them around, we want to preserve their state. This is where Portals come in.

Let's redo the example above with portals. As you click on the squares, you’ll see that the video continues uninterrupted.

The rest of this section will explore various ways to make portals in Deku.


Global portals

Global portals are the easiest to work with, but they come with a potential performance price. Let's dive into their syntax!

The global portal syntax

The global portal syntax looks a lot like the hooks syntax. We use a left-bind in a Deku.do block to create a value that will be used later. But instead of creating a hook, we create a component. In the example below, look how globalPortal1is used to create a single component that is consumed by other components.

And, as y'all know, the result is the following.

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

import Prelude
import Deku.Toplevel (runInBody)

import Data.Tuple.Nested ((/\))
import Deku.DOM.Attributes as DA
import Deku.Control (portal, text_)
import Deku.Core (Nut)
import Deku.DOM as D
import Deku.Do as Deku
import Deku.Hooks (guard, useState)
import Deku.DOM.Listeners as DL
import Deku.Toplevel (runInBody)
import Effect (Effect)
import ExampleAssitant (ExampleSignature)
import FRP.Poll (Poll)
import Deku.Toplevel (runInBody)

data Square = TL | BL | TR | BR

derive instance Eq Square

moveSpriteHere
  :: { video :: Nut
     , square :: Poll Square
     , setSquare :: Square -> Effect Unit
     , at :: Square
     }
  -> Nut
moveSpriteHere { video, square, setSquare, at } = D.a
  [ DL.click_ \_ -> (setSquare at)
  , DA.klass_
      "block max-w-sm p-6 bg-white border border-gray-200 rounded-lg shadow-md hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700"
  ]
  [ D.h5
      [ DA.klass_
          "cursor-pointer mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white"
      ]
      [ text_ "Move sprite here"
      , guard (square <#> (_ == at)) video
      ]
  ]

myVideo :: Nut
myVideo = D.video
  [ DA.width_ "175"
  , DA.height_ "175"
  , DA.autoplay_ "true"
  , DA.loop_ "true"
  , DA.muted_ "true"
  ]
  [ D.source
      [ DA.src_ "https://media.giphy.com/media/IMSq59ySKydYQ/giphy.mp4" ]
      []
  ]

main :: Effect Unit
main = void $ runInBody Deku.do
  vid <- portal myVideo
  setSquare /\ square <- useState TL
  D.div [ DA.klass_ "grid grid-cols-2" ]
    [ moveSpriteHere { video: vid, square, setSquare, at: TL }
    , moveSpriteHere { video: vid, square, setSquare, at: TR }
    , moveSpriteHere { video: vid, square, setSquare, at: BL }
    , moveSpriteHere { video: vid, square, setSquare, at: BR }
    ]

While using a portal looks like using a vanilla Deku node, don't let it fool you! These two snippets are different:

let a = Deku.div_ [ text_ "hi" ]
a <- globalPortal1 (Deku.div_ [ text_ "hi" ])

Portals are created with a left bind, and as such are referentially opaque. That means that, when a portal is created, it represents a single DOM node that exists, as opposed to a template for a DOM node.

Relating this concept to the example above, let's zoom in on the following line of code:

vid <- globalPortal1 myVideo

The term vid is one single instantiation of our video. On the other hand, if we had passed myVideo to the moveSpriteHere function, we would be working with a referentially transparent object that acts as a blueprint for a node to construct as opposed to an actual node.

Because DOM nodes can only appear in one place in the DOM, when a portal is used multiple times, the poll is undefined. For that reason, in your application logic, it is important to make sure that a portal only ever appears in at most one place.

Performance considerations

As we learned in the State section, hooks can hold terms of any type. Hooks can even contain Deku components, which means that a component can be pushed to a pusher and sent to an event. For this reason, once a global portal is created, it can theoretically be injected anywhere in an application and thus must be kept alive for the lifespan of the application. In many scenarios, for example an in-app chat widget, this is fine. But in many scenarios, like if a portal exists in a list element, you’ll want the portal to be cleaned up when the element is cleaned up. For that, we have local portals, which solve that problem at the expense of a slightly clunkier syntax.

Previous
Pursx