Coder Social home page Coder Social logo

Fix memory management gotcha about laminar HOT 3 CLOSED

raquo avatar raquo commented on May 18, 2024
Fix memory management gotcha

from laminar.

Comments (3)

raquo avatar raquo commented on May 18, 2024

Well, damn, this is indeed really hard.

Let's summarize the problem once again: when we create a Laminar element like so:

val el = div(onClick --> clickStream, child.text <-- textState)

we immediately create the required subscriptions for clickStream and textState. We will kill these subscriptions when el becomes unmounted, except this line of code does not actually mount it into the DOM, it merely creates the element. So it is entirely possible that el will never be mounted, and therefore that it will never be unmounted, and its subscriptions will live on forever, screwing up our logic and causing memory leaks.

The solution I was hoping to get working is similar to DynamicSubscription feature of Laminar v0.2 – basically, don't start the subscriptions until the element is mounted. Sounds simple enough – we can now safely discard el without mounting it. However, there's a deeper, nastier problem with this. Laminar elements are Owner-s, and we need those not just for subscriptions, but also for creating state variables such as textState. Indeed, it is easy to imagine a method like this:

def makeDiv(textSignal: Signal[String]): Div = {
  val el = div("Number of people:")
  val textState = textSignal.toState(owner = el) // imagine there's a reason to make it a State
  el <-- child.text <-- textState
  el
}

Same situation as before – simply calling that method and ignoring its output is enough to cause a memory leak (textState will never stop, and thus textSignal won't, either).

However, observe how we made el the Owner of textState. State gives us certain guarantees – namely, that once instantiated it will always run until its owner tells it to permanently shut down. The proposed DynamicSubscription would be incompatible with this guarantee, making State effectively indistinguishable from the lazy Signal.

Basically, to solve this memory management issue for good, we can't have Laminar elements remain Owners. Perhaps this calls for introduction of some kind of Component type, although I'm not quite sure what role it would play other than somehow provide an owner and maybe manage element lifecycle (given that this is what tracks mounting / unmounting).

This could potentially improve performance too, as we would only need to keep track of Components' root elements mounted status. I do not want to give up Laminar's compositional freedom. Currently you do not need to wrap your code in any wrappers, and can freely use functions / classes / objects / whatever to build your components. React's notion of components does not suit me at all, it's too constrained.

Less radically, I think perhaps we could introduce a Fragment[El <: ReactiveElement] type like Owner => El. We would recommend using this type for any reactive elements that you aren't 100% sure will be eventually mounted. We wouldn't be able to enforce it, but it is a relatively easy rule of thumb to follow, similar to don't pass State to a different owner's context, pass Signals instead that we already have.

Another option could be to provide something of a whenMounted hook, like so:

val el: Div = div.whenMounted { (owner, thisNode) =>
  val textState = textSignal.toState(owner) // imagine there's a reason to make it a State
  List(
    "Hey, "child.text <-- textState
  )
}

This seems like what people would generally intend state to work like, however this makes it hard to make a component that returns not just an element but also other data like perhaps some relevant streams that are generated from its state. So, this actually sounds like a use case for a well designed Component type. Hrmmmm.

I need to sleep on this... repeatedly.

from laminar.

raquo avatar raquo commented on May 18, 2024

The Component / Fragment approach looks suspiciously like IO :|

Kinda want to avoid going in that direction, having everything wrapped in a monadic type like that.

from laminar.

raquo avatar raquo commented on May 18, 2024

A few more things for my own clarity:

  • <-- and --> work just fine in terms of memory management, if we switch to a model where we start those subscriptions when the relevant element is mounted (as opposed to when it's created)
  • It's the "component's" state that we have a problem with. Ideally, elements should not be owners, they should contain a private owner and expose safe hooks to use it.
  • <-- and --> are some of those, but I think we need another one that would let you define Owner => A, where the Owner param is provided by the element, and A is an arbitrary output type.
  • A needs to be arbitrary in order to preserve easy composition. Perhaps it should be (owner, thisNode) => A instead, so that we can return thisNode is this is what we need. But we should also be able to return other things like bundles of nodes and streams.
  • This still feels too restrictive, however. If we go this way, a component will still need to be defined by a single element. Perhaps a Component type wrapping a Owner => A is better still, but it would require an Owner that is not related to a particular Element. The Component would need to be a Mod, I guess, similar to how an element is a Mod. The component would then be able to subscribe to its parent's mount events, so it would not become a toxic type like IO.
  • If we switch to subscriptions activating / deactivating onMount, we will need to provide convenience methods around that. If you unmount a component in React, all its state is lost. In Laminar, we can choose what we want to do.
  • None of this forces us to solve re-mounting unmounted elements. Component could potentially discard and re-run all of its contents when it's mounted again to get a clear state. But should it? I don't think so. This is all too abstract right now.

from laminar.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.