Vars and Signals in Laminar
I’ve started building the frontend of my side project with Laminar, and its reactive core is small enough to fit in a short note. There are basically two types to understand.
Var: State You Can Write
A Var is a reactive variable, a value holder you can read, write, and observe:
1val count = Var(0)
2
3count.set(5)
4count.update(_ + 1)
Signal: State You Can Watch
A Signal is the read-only view of a value that changes over time. Every Var exposes one:
1val count = Var(0)
2val doubled: Signal[Int] = count.signal.map(_ * 2)
A Signal always has a current value, and you can derive new Signals from it with map, combineWith, etc. This is where it clicks: your UI state becomes a graph of derived values, and Laminar keeps the graph consistent for you.
Binding to the DOM
The interesting part is that you don’t read signals in your view code, you bind them, with the <-- and --> arrows:
1def counter(): HtmlElement =
2 val count = Var(0)
3 div(
4 button("+", onClick --> { _ => count.update(_ + 1) }),
5 p(
6 "Count is: ",
7 child.text <-- count.signal.map(_.toString)
8 )
9 )
child.text <-- signal means: whenever this signal emits, update this text node. Not the component, not the subtree, that one text node.
This is the fundamental difference from React. The counter() function runs once. There is no re-render, no virtual DOM diffing, no memoization to get wrong. The signal graph knows exactly which DOM node depends on which piece of state, so an update is a direct write to the right node.
Coming from React, the mental shift is that a component isn’t a function you re-run, it’s a function that runs once and wires things up. After a week or so this stopped feeling exotic and started feeling obvious.
One thing I’m deliberately hand-waving here: who manages all those subscriptions, and why don’t they leak when elements get removed? Laminar has a whole ownership system for that, and it deserves its own note once I understand it properly.