Home
Apr 5, 2026

Ownership in Laminar Explained Simply

scalascalajslaminarmemory

I promised myself I’d figure out how Laminar’s ownership system actually works instead of just enjoying that it does. Here’s what I found.

The Problem It Solves

Every <-- binding creates a subscription: a callback registered on a signal or stream. In any long-running FRP system, subscriptions are the classic leak: you remove a component from the page, but a signal somewhere still holds a reference to its callback, so the component (and every DOM node it references) can never be garbage collected. Do that in a list that re-renders a few hundred times and your tab quietly eats memory.

Frameworks usually solve this with lifecycle hooks you must remember to use, useEffect cleanup functions in React, onDestroy in others. Forget one, and you leak.

Laminar’s Answer: No Orphan Subscriptions

In Laminar (well, in Airstream, its reactive layer), it is impossible to create a subscription without an Owner:

1val sub = signal.foreach(println)          // does not compile
2val sub = signal.foreach(println)(owner)   // an Owner is required

An Owner is an object responsible for killing the subscriptions it owns. The rule isn’t a convention or a lint warning, it’s in the type signature. You physically can’t write the leaking version.

Elements Are the Owners

So who owns the subscriptions you create with <-- inside an element? The element itself:

1div(
2  child.text <-- count.signal.map(_.toString)
3)

The binding doesn’t activate when this line runs. It activates when the div is mounted into the real DOM, and it’s deactivated when the div is unmounted. The element’s lifecycle is the subscription’s lifecycle:

  • Mounted → subscriptions start, the signal begins doing work.
  • Unmounted → subscriptions stop, references are released.

The elegant consequence: an unmounted subtree costs nothing. If a signal’s only listeners were in a component you removed, the signal stops computing entirely, laziness falls out of the ownership design for free.

When You’re Outside an Element

Occasionally you need a subscription with no element in sight (a global keyboard shortcut, a router). For that there’s the escape hatch:

1val owner = new OneTimeOwner(() => ())
2route.foreach(render)(owner)

Or unsafeWindowOwner, which lives as long as the page does. The naming does the documentation: anything with an explicit owner outlives components, and unsafe tells you whose problem that now is.

What I like most is the philosophy underneath: the resource-management burden that other frameworks put on runtime discipline (“remember your cleanup function”), Laminar moves into the type system. The same trick Scala plays everywhere else, applied to memory leaks in the browser.