New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Manual clean-up #5
Comments
From the S.js overview:
This sounds like what I'm looking for, but I'm not sure. Thoughts? 🙂 |
Hmm, so at closer inspection, I think S.js has the same problem - automatic disposal happens only in parent/child relationships, you still have to manually dispose of the parents. So I got to thinking, can we do something with Of course, what we want is a reference to the value, not the key, and This lead me to wonder if there was a polyfill, which there is - this relies on var wr = new WeakMap;
function WeakRef(value) {
wr.set(this, value);
} That is, you keep a I don't know if this approach actually works though - I tested it, and it didn't seem to work for me in the current version of Chrome, so I'm going to ask the author of the polyfill how he tested this. Either there's a bug or this approach might not work at all. 🤔 |
Hi Rasmus, thanks for your interest to the library! Right, unlike S.js, dipole doesn't implement any parent/child relationships between reaction objects, but it's possible to do some with basic OOP patterns over About WeakRef, it's not that simple :) |
You're right, this is a dead end - neither of the two polyfills out there actually work. (No idea why they exist to begin with.) |
Hmm, so I've tried to implement manual clean-up, but it seems https://codesandbox.io/s/reactive-dom-lol-gc-v046n?file=/src/index.jsx I added a list of reactions to a property on the DOM element - if the DOM element gets replaced during a reaction, I recursively I added console output, so you can see when created reactions get tracked and released - it looks like there should still be tracked reactions that haven't been released, but for some reason, it only seems to work after the destroy/create cycle for each reaction. Could this be because I'm creating nested reactions? Creating new reactions during a call to a reaction, that is. I noticed a difference from S.js, which immediately calls your reactions the first time, and your library, where I have to manually call Or maybe I'm doing something incredibly dumb and this approach doesn't work at all. 😄 |
For some reason I can't get your sandbox to output anything to result screen. import { Reaction } from 'dipole';
let gReactionContext = null;
class NestedReaction extends Reaction {
constructor(...args) {
super(...args);
this.children = [];
if (gReactionContext !== null && gReactionContext instanceof NestedReaction) {
gReactionContext.addChild(this);
}
}
addChild(child) {
this.children.push(child);
}
run(...args) {
this.destroyChildren();
const oldReactionContext = gReactionContext;
gReactionContext = this;
try {
return super.run(...args);
} finally {
gReactionContext = oldReactionContext;
}
}
destroyChildren() {
this.children.forEach((child) => child.destroy());
this.children = [];
}
destroy() {
this.destroyChildren();
super.destroy();
}
}
function reaction(...args) {
return new NestedReaction(...args);
} When a reaction gets created while another reaction is running, it's registered as a child of this reaction. When parent reaction runs, it destroys all child reactions first. |
Yeah, that happens. CSB is just so broken these days. Clear your application state from dev tools and reload the whole thing - sometimes, still nothing comes up, you may have to press the refresh-button in the "browser" in CSB. Your nested reactions looks nice. I will give that a try. 👍 |
Updated |
I commented-out the I don't completely understand how. I'm doing nothing to track or release any listeners. I don't fully understand how this library works, but can it really be this easy? I would have thought I would need to do something to dispose of at least the top-level reactions? As I understand it, a reaction is basically creates a sort of closure around the observables it depends on, right? How are they getting unsubscribed now? It can't be this easy? What am I missing?? 😄 |
Confirmed no memory leaks - the garbage collector is doing it's job. This is me going nuts on the input, changing it between "World" and "World2" at least 100 times, and then clicking like crazy on the checkbox. There might be circular references, and things clearly stack up for shorter periods before getting collected, but it doesn't appear to be leaking. This library is magical. I'm still waiting for the other shoe to drop. I must be missing something. 😄 If this really "just works", is there any reason not to promote this from an extension to a standard feature in the library? |
Do you mean with the Logic of
import { NestedReaction } from 'dipole/addons`; , so it won't be included by default in bundle that I'm trying to keep small-ish. |
S.js highlights this as a qualitative feature - though I guess that might be a matter of opinion. It doesn't seem like this would add more than a few lines if it were built-in? Making Your call of course :-) I added stateful components and still the whole thing is just ~60 lines of code. I was very much expecting things to break because I'm creating observables inside the component "instances" now - but the garbage collector still seems to be doing it's job. I remain suspicious because this seems waaaay too good to be true. 😄 |
Do you mean with the Logic of
import { NestedReaction } from 'dipole/addons`; , so it won't be included by default in bundle that I'm trying to keep small-ish. |
I could not understand the But honestly, it's 10 lines of code: This wouldn't leave more than a few bytes of footprint. I honestly still think this is a "qualitative" feature that shouldn't be optional or left to an add-on. I wouldn't want to leave users of the library to discover on their own (the painful way) why the library leaks memory for their use-case. (Note the missing type-check |
So finally, you've convinced me :) I added the nested reaction behaviour as default in dipole since version 2.2.0, so no In case old behaviour is needed, it can be emulated using untracked transactions: const free = utx(() => reaction(() => { ...reaction body... })); |
Nice, elegant solution 👍 |
Tested and can confirm no memory leaks or any adverse results from switching to I think we can close this issue? 🙂 |
Hey,
This library is interesting as as state management solution for React/Preact - but I'm really interested in using this library to create a truly reactive UI library, one that doesn't diff virtual DOM nodes, along the lines of Sinuous.
I cobbled together a working prototype last night, and this was surprisingly easy:
https://codesandbox.io/s/reactive-dom-lol-rrtd4?file=/src/index.jsx
The problem is, this leaks listeners like crazy - if you make a few changes to the input value, and then toggle the checkbox, you can see the "LOL" messages piling up in the console. Reactions aren't getting garbage-collected, presumably because they internally reference the observables they're listening to.
From the documentation, I understand I'd have to manually
destroy()
the reactions to free them - which I've tried to do, but suddenly everything is much more complex than it seems on the surface, as I basically have to manually manage the whole create/update/destroy life-cycle using code.I could be wrong, but I don't think S.js has this issue? Not sure.
I wonder if there would be something we can do with
WeakMap
to enable garbage-collection of reactions - or maybe avoid keeping subscriber references by decoupling their relations through a message bus or something?I honestly don't understand the theory behind it all, I just know I like it. 😄
Is this something you're interested in at all, or is this mostly a state management library to you? 🙂
The text was updated successfully, but these errors were encountered: