Comments (3)
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.
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.
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 defineOwner => A
, where theOwner
param is provided by the element, andA
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 returnthisNode
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)
- Possible O(n^2) instead of O(n) cost ? HOT 13
- Airstream semantics update – RFC
- Safari cursor jumps to the end when an input value is set HOT 2
- Errors in recursive graphs can cause stack overflow HOT 1
- Docs: Conditional rendering
- Moving elements from one `children` list to another HOT 4
- Laminar is a Scala.js reactive library [SEO] HOT 1
- Virtual dom link in quick start docs is broken HOT 2
- Give `$` back to Scala compiler
- RFC: Alternative server-side rendering (SSR) architecture HOT 3
- RFC: signal.peekNow() HOT 6
- Docs: Algolia search
- RFC: Drop Scala 2.12 support HOT 2
- Laminar 15.0.0 pre-release testing & issues HOT 19
- Animation features
- [Question] - jQuery bind and oninput event HOT 2
- [DOM] Using certain reserved values for the `name` attribute breaks Scala types HOT 1
- onParentChange callbacks HOT 3
- Laminar Native for building mobile and desktop apps HOT 2
- ZIO / FS2 / etc. integration examples HOT 5
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
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.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from laminar.