FS2 streams with Scala JS

FS2 streams with Scala JS

I’ve recently been writing a small Scala JS frontend application to provide an administration interface to an API for a side project and have had a good experience doing so, owing to the surprisingly (to me, at least!) comprehensive support for Scala JS amongst cats, cats-effect and fs2. I've found that using these libraries along with scalatags has been a good learning experience and I thought I'd share my discoveries here.

I'm using the following dependencies throughout the code examples below:

ScalaJS' DOM library itself sticks closely to the native JavaScript DOM API which can make it difficult to write idiomatic applications using cats-effect but with the help of fs2 it is relatively easy to represent DOM events as streams through the use of a queue to capture events from ScalaJS event handlers (classic Event => Unit callbacks):

Another way fs2 can help is with Stream.bracket - a function that assists in managing state by providing the means to acquire a resource for use in a stream before having it automatically cleaned up when the stream terminates. I’ve found this means it is practical to model UI components in frontend applications as fs2 streams - Stream.bracket can be used to add the DOM nodes for the component to the page during the acquire step before providing events from the nodes as a stream. The previously added nodes will then be removed from the page when the event stream is terminated:

Finally fs2 offers a number of stateful stream transforms, such as mapAccumulate which can be used to manipulate state based on events fired by the DOM. Here's an example of a tiny app that displays how often a button is clicked in another button, which itself is removed by terminating its stream when it is clicked:

def run(args: List[String]): IO[ExitCode] =
// add a button to the DOM
button[IO](body, "Click me")
// keep track of how often it is clicked
// and emit only that number downstream
.zipWithIndex.map(_._2)
// add a button that will be removed after one click.
// using switchMap means this stream will be interrupted
// if the first button is clicked, so the count button will update
.switchMap(ts => button(body, s"Clicked $ts times").take(1))
.compile.drain.as(ExitCode.Success)

The boilerplate involved in bridging the divide between DOM functions and streams is a definite disadvantage of this approach but I think once it is written the resulting application code can end up being quite reasonable, though I've really only just begun exploring this way of writing apps.

One final disadvantage of ScalaJS I've begun to hit against is the relative lack of facades for popular JavaScript libraries. There is, however a useful list and I think that if you're looking for some easy open source contributions that writing some facades is a good place to start!