Slides, contact etc.: https://linktr.ee/kubukoz
Check out the recording, slides and links later!
// core idea def div(...): Resource[IO, HtmlElement[IO]]
cats.effect.Resource
// simplified trait Resource[A] { def use[B](f: A => IO[B]): IO[B] } object Resource { def make[A](acquire: IO[A])(release: A => IO[Unit]): Resource[A] }
Encapsulates the lifecycle of a stateful resource: allocation -> usage -> cleanup.
mkConnection.use { conn => makeClient(conn).use { client => makeServer(conn).use { server => client.call(server) } } }
Looks familiar...
flatMap
val myApp = for { conn <- mkConnection client <- makeClient(conn) server <- makeServer(conn) } yield (client, server) myApp.use { (client, server) => client.call(server) }
...on steroids!
val myComponent1 = for { d <- div("Hello, world!") b <- button("Click me!") c <- div(d, b) } yield c // or, usually better: val myComponent2 = div( div("Hello, world!"), button("Click me!") ) myComponent2.renderHere(node)
How do we make it interactive?
button( onClick(IO(dom.window.alert("Button clicked!"))), "Click me!" ) .renderHere(node)
input.withSelf { self => onChange(self.value.get.flatMap { value => IO(dom.window.alert(value))}) } .renderHere(node)
(similar to useRef in React)
useRef
Hint: it's the same as with any other cats-effect application.
cats.effect.Ref!
cats.effect.Ref
// simplified trait Ref[A] { def get: IO[A] def update(f: A => A): IO[Unit] } Ref[IO].of(initialValue: A): IO[Ref[A]]
Recommended talk: Shared state in pure FP
Ref
val mkRef: IO[Ref[IO, Int]] = Ref[IO].of(0) // results in 1 mkRef.flatMap { ref => ref.update(_ + 1) *> ref.get } // results in 0 mkRef.flatMap(_.update(_ + 1)) *> mkRef.flatMap(_.get)
To share a Ref, you need to pass it around after creating it once.
Resource.eval(Ref[IO].of("")).flatMap { ref => div( input.withSelf { self => ( // set onInput(self.value.get.flatMap(ref.set)), placeholder := "What's your name?" ) }, button( "Submit", // get onClick(ref.get.flatMap(name => IO(dom.window.alert(s"Hello, $name!")))) ) ) } .renderHere(node)
But how do we display it as it changes?
Ref[IO].of(0).toResource.flatMap { ref => div( "Counter: ", ref, // compile error button( onClick(ref.update(_ + 1)), "Increment" ) ) }
allow us to get a value to be displayed + notify us when the state changes
fs2.concurrent.Signal
// simplified trait Signal[A] { def continuous: fs2.Stream[IO, A] def discrete: fs2.Stream[IO, A] def get: IO[A] }
fs2.Stream
.hold
SignallingRef
Signal
Stream
fs2.Stream .awakeEvery[IO](100.millis).holdResource(0.seconds) .flatMap { signal => div( "The presentation has been running for ", signal.map(_.toSeconds.toString), " seconds." ) } .renderHere(node)
Acts like a Ref, is also a Signal
SignallingRef[IO].of(0).toResource .flatMap { ref => button( onClick(ref.update(_ + 1)), styleAttr := "font-size: 1em", // Signal#map ref.map(_.toString), " clicks" ) } .renderHere(node)
For example, styling:
val component = SignallingRef[IO].of(0).toResource.flatMap { ref => button( onClick(ref.update(_ + 1)), "Clicks: ", ref.map(_.toString), styleAttr <-- ref.map(s => s"font-size: ${(s + 1)}em") ) } div(component, component).renderHere(node)
Courtesy of fs2-dom
val mouseEvents = fs2.dom.events[IO, dom.MouseEvent](dom.document, "mousemove") mouseEvents .map(e => (e.clientX, e.clientY)) .holdResource((0, 0)) .flatMap { sig => div(sig.map(_.toString)) } .renderHere(node)
def keyboardEvent(key: Char, tpe: String) = fs2.dom.events[IO, dom.KeyboardEvent](dom.document, tpe).filter(_.key == key.toString) def keyEvents(key: Char) = keyboardEvent(key, "keydown").either( keyboardEvent(key, "keyup") ).map(_.isLeft) def showBool(b: Boolean) = if b then "" else "" def showLetter(k: Char, state: Signal[IO, Boolean]) = div(code(k.toString), ": ", state.map(showBool))
keyEvents('k') .holdResource(false) .flatMap(showLetter('k', _)) .renderHere(node)
What if we want to track more keys?
"qwerty" .toList // ! .traverse { key => keyEvents(key).holdResource(false).tupleLeft(key) } .flatMap { signals => div(signals.map(showLetter)) } .renderHere(node)
Like a WebSocket stream?
val client = WebSocketStreamClient[IO].withFallback(WebSocketClient[IO]) val wsMessages = client.connectHighLevel(WSRequest(uri"ws://localhost:8080")) .flatMap { _.receiveStream.collect { case WSFrame.Text(text, _) => text } .filterNot(_.isBlank) .sliding(10).map(lines => div(lines.map(p(_)).toList)) .metered(1.second / 10) .holdResource(div("")) }
def shrek(node: org.scalajs.dom.Element) = { div( IntersectionObserver.isVisible(node).map { visibleSig => visibleSig.map { case true => div(wsMessages) case false => div("") } } ) }.renderHere(node)
// This will not work if you're viewing the slides online. // Please watch the recording! shrek(node)
Resource
also shows separation of refs
Monadic composition of Resource in a traverse
WebSocketStreamClient: Implementation of unstable web API: https://github.com/http4s/http4s-dom/pull/384
this is what powers our http4s servers etc.