Core Concepts
All of Calico’s core concepts are actually inherited from Cats Effect and FS2. This page highlights them and demonstrates how they are applied in the context of building web applications.
Components and resource management
The most important idea behind Calico is that each component of your app (and in fact your app itself) should be expressed as a Resource[IO, HtmlElement[IO]]
.
import cats.effect.*
import fs2.dom.*
// note: no calico import yet!
val component: Resource[IO, HtmlElement[IO]] = ???
// or more generally:
def component[F[_]: Dom]: Resource[F, HtmlElement[F]] = ???
This Resource
completely manages the lifecycle of that element and its children. When the Resource
is allocated, it will create an instance of the HtmlElement
and any supporting resources, such as background Fiber
s or WebSocket connections. In kind, when the Resource
is closed, these Fiber
s and connections are canceled and released.
Because Resource[IO, HtmlElement[IO]]
is referentially-transparent, it naturally behaves as a "builder". Your component can be re-used in multiple places in your application as well as un-mounted and re-mounted without worrying about crossed-wires or leaked resources. This makes it easy to compose components.
So far, none of this is specific to Calico: we get all of this for free from Cats Effect and FS2 DOM. Calico provides an idiomatic DSL for describing components with standard HTML tags and attributes.
import calico.html.io.{*, given}
import cats.effect.*
import fs2.dom.*
val component: Resource[IO, HtmlElement[IO]] = div(i("hello"), " ", b("world"))
Yes, in this very unexciting example i("hello")
and b("world")
are both Resource
s that monadically compose with div(...)
to create yet another Resource
! There are no other resources involved in this very simple snippet. Also note that we have not yet created any HtmlElement
s, we have merely created a Resource
that describes how to make one.
A more interesting example is this interactive Hello World demo.
import calico.*
import calico.html.io.{*, given}
import calico.syntax.*
import cats.effect.*
import fs2.*
import fs2.concurrent.*
import fs2.dom.*
val component: Resource[IO, HtmlDivElement[IO]] =
SignallingRef[IO].of("world").toResource.flatMap { name =>
div(
label("Your name: "),
input.withSelf { self =>
(
placeholder := "Enter your name here",
// here, input events are run through the given Pipe
// this starts background fibers within the lifecycle of the <input> element
onInput --> (_.foreach(_ => self.value.get.flatMap(name.set)))
// You can also use simpler syntax - these are all equivalent to a foreach:
// onInput(_ => self.value.get.flatMap(name.set))
// onInput(self.value.get.flatMap(name.set))
)
},
span(
" Hello, ",
// here, a Signal is rendered into the HTML
// this starts background fibers within the life cycle of the <span> element
name.map(_.toUpperCase)
)
)
}
The ideas are very much the same as the prior example.
input(...)
is aResource
that creates an<input>
element and also managesFiber
s that handle input events.span(...)
is aResource
that creates a<span>
element and also managesFiber
s that handle rendering of the name.div(...)
is aResource
composed of theinput(...)
andspan(...)
Resource
s, and therefore (indirectly) manages theFiber
s of its child components.
And there we have it: a self-contained component consisting of non-trivial resources, that can be safely used, reused, and torn down.
Signals
In the Hello World demo above, we glossed over the SignallingRef
used to hold the component’s state. A SignallingRef
is a Cats Effect Ref
(i.e. a mutable variable) that is also an FS2 Signal
.
A Signal
is a time-varying value. You can always obtain its current value, and you may also subscribe to a stream of update events that notify when it is modified. This is ideal for use in UI components: they can always render immediately with the current value, and re-render only when there are updates.
Signal
is a monad, enabling them to be transformed with pure functions and composed with each other. Using transformation and composition, you can derive a Signal
that contains precisely the data you are interested in.
import cats.syntax.all.*
import calico.frp.given
enum Cardinal:
case North, South
val signals = (
SignallingRef[IO].of(Option.empty[Cardinal]),
SignallingRef[IO].of(""),
SignallingRef[IO].of(""),
).tupled.toResource
val app: Resource[IO, HtmlDivElement[IO]] =
signals.flatMap { (cardinalSig, northSig, southSig) =>
div(
div(
label("North input: "),
input.withSelf { self =>
onInput(self.value.get.flatMap(northSig.set))
},
),
br(()),
div(
select.withSelf { self =>
(
option(disabled := true, selected := true, "Select input"),
option(value := "north", "North"),
option(value := "south", "South"),
onChange {
self.value.get.map {
case "north" => Some(Cardinal.North)
case "south" => Some(Cardinal.South)
case _ => None
}.flatMap(cardinalSig.set(_))
}
)
},
" ",
// compose cardinal signal with appropriate input signal
(cardinalSig: Signal[IO, Option[Cardinal]]).flatMap {
case Some(Cardinal.North) => northSig
case Some(Cardinal.South) => southSig
case None => Signal.constant("")
}
),
br(()),
div(
label("South input: "),
input.withSelf { self =>
onInput(self.value.get.flatMap(southSig.set))
},
),
)
}
There are various ways to obtain a Signal
.
-
Create a
SignallingRef
with an initial value.SignallingRef[IO].of("initial value")
-
Derive a
Signal
from aStream
, by “holding” its latest value.def stringStream: Stream[IO, String] = ??? stringStream.holdResource("initial value") stringStream.holdOptionResource // use None for the intitial value
Task scheduling and glitch-free rendering
A JavaScript webapp typically has a flow like:
-
An event fires. Examples:
- a user event, such a button click
- a scheduled timer event
- an I/O event, such as an HTTP response or WebSocket message
- An event handler is triggered, starting (potentially concurrent) tasks to update the application state and the UI. These tasks may also setup new event emitters, for example by scheduling timers or initiating an HTTP request.
- The UI re-renders.
Calico is highly-optimized for this pattern and by default schedules all tasks as so-called microtasks. These microtasks have very high-priority: while there is still work to be done, the UI will not re-render and no further events will be processed. Only once all microtasks are complete, will the UI re-render and events will start being processed again.
Notice that this scheduling strategy guarantees glitch-free rendering. Because all tasks triggered by an event must complete before the view re-renders, the user will never see inconsistent state in the UI.
However, there are certain situations where you may want the browser to re-render in the middle of a task. In these cases, simply sequence an IO.cede
operation. This will temporarily yield control flow back to the browser so that it may re-render the UI, before resuming the task.
updateComponentA *> // doesn't render yet
updateComponentB *> // still didn't render
IO.cede *> // re-render now
doOtherStuff *> ... // do non-view-related work
Explicitly inserting an IO.cede
can be a useful strategy to improve your app’s UX, by re-rendering as soon as you are done updating the view, and deferring other work until after the re-render. This will make your UI more responsive.
To learn more I recommend this article about the JavaScript event loop.