-
Notifications
You must be signed in to change notification settings - Fork 65
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
Computations creating signals #21
Comments
Data signals -- what you get from either Can you say more about the "rare cases" where you saw weird behavior? I might have a guess. If you're creating data signals inside computations, the part that sometimes gets tricky is that if the computation updates, new data signals are created, which therefore reset to whatever initial state is assigned, making it seem like their state never changes. For instance, I once made a component that looked about like this: function ClickMe() {
const clicked = S.data(false);
return clicked() ? <div>Clicked</div> : <div onClick={() => clicked(true)}>Click Me</div>;
} So the intent there is that it creates a div that says "Click Me" until you click it, at which point it creates a div that says "Clicked." But that's not what happens :). Since The real problem was that I mis-defined function ClickMe() {
const clicked = S.data(false);
return S(() => clicked() ? <div>Clicked</div> : <div onClick={() => clicked(true)}>Click Me</div>);
} Does that sound relevant to your situation? If you have a code sample where data signals created in computations weren't working the way you thought they would, post it up and I'll take a look. |
EDIT: Follow Adam's comment first. It's more likely to help you with your specific issue. I go into explaining a bunch of stuff that is probably unnecessary to solve your problem. It's not the nested signals but the nested computations that are of interest here. Signals can freely be collected when they move out of scope. However with computations it's possible to create dependencies that exist outside of their calling scope. If they are not disposed further changes to signals will continue to trigger them even after you've lost variable reference to them. Luckily S is setup so that on every computation re-run and disposal it disposes of all dependencies and child computations. As you can imagine this cascades since the disposal of the child computation disposes it's dependencies and it's child computations and so on. So this holds up in the majority of cases. But it also means you are always redoing the work. So consider a conditional in view (this isn't exactly how it works but I think it conveys the point): const count = S.data(6);
const parentNode = document.createElement('div')
S(() => {
if (count() > 5) {
parentNode.textContent = '';
const node = document.createElement('div');
S(() => node.textContent = 'Tracked ' + count() + ' times.');
parentNode.appendChild(node);
} else {
parentNode.textContent = 'Too Low';
}
}); Now your intention here is to only draw the child if the count is bigger than 5 and then update the count in the child div as the value changes. However every time the count changes the outer computation retriggers and every thing is redone. It is possible to hoist this inner work out to make this work but I want to show you the other way which is more the way a specialized if method would work (or the way we could memoize values in an array). const count = S.data(6);
const parentNode = document.createElement('div')
let dispose;
S.cleanup(() => dispose && dispose());
S( prev => {
const result = count() > 5;
if (result === prev) return prev;
dispose && dispose();
if (result) {
S.root(disposer => {
dispose = disposer;
parentNode.textContent = '';
const node = document.createElement('div');
S(() => node.textContent = 'Tracked ' + count() + ' times.');
parentNode.appendChild(node);
});
} else {
parentNode.textContent = 'Too Low';
}
return result;
}); Ok a lot more here. See the problem is to prevent repeat work we need to exit early if value of the condition hasn't changed. The problem is the nested computations would be released on re-run and the nested view wouldn't update anyway. By placing them in their own root the outer computation is no longer responsible for disposing them. However, now you are. So you need to make sure you dispose on re-run where applicable or if the outside context would ever re-run. Truthfully though unless you are writing like array mapping control flow or trying to memoize nested values you should almost never need to make the roots yourself. |
@adamhaile addressing your response first (holy cow, having a response from both of you so fast is just wonderful) Yes, that sounds like exactly the issues with which I am dealing, thank you for being intuitive enough to see into my question. I had a little signal machine function (a function that returned some new signals plus computations in a plain JS object), and it was computed in an The takeaway I am getting from you Adam is that in these cases it is often necessary to make sure that these types of functions return a computation based on the new state signals, and that returned S.js computation node should then update properly when the new signals change. Ok, on to @ryansolid's comment. |
Ryan, interesting example. An equivalent of your first case that does it all in JSX would be: const count = S.data(6);
const parentNode = <div>{count() > 5 ? <div>Tracked {count()} times</div> : 'Too Low'}</div>; As you say, that would re-create the inner You could fix some of that by doing: const count = S.data(6);
const childNode = <div>Tracked {count()} times</div>;
const parentNode = <div>{count() > 5 ? childNode : 'Too Low'}</div>; That's still not perfect: You could get the efficiency right at the expense of synchronicity by piping the test into an const count = S.data(6);
const isBigEnough = S.value(true);
S(() => isBigEnough(count() > 5));
const parentNode = <div>{isBigEnough() ? <div>Tracked {count()} times</div> : 'Too Low'}</div>; Subclocks would fix the synchronicity issue (though again, they're not quite ready for production): const count = S.data(6);
const isBigEnough = S.subclock(() => {
const isBigEnough = S.value(true);
S(() => isBigEnough(count() > 5));
return isBigEnough;
});
const parentNode = <div>{isBigEnough() ? <div>Tracked {count()} times</div> : 'Too Low'}</div>; If there were an S utility, called, say, const count = S.data(6);
const isBigEnough = S.expr(() => count() > 5);
const parentNode = <div>{isBigEnough() ? <div>Tracked {count()} times</div> : 'Too Low'}</div>; That would get maximum efficiency :). |
Wow @ryansolid you just answered a question I had posed in your jsx babel repo, regarding when it is necessary and why to do manual disposing with S.js dom reconcilation, and as usually you explained things extremely well. This is something I really need to be aware of. I am using Surplus's runtime (brought directly into my app by copying the source :) ) and I set up |
Thanks Adam. I chose that example because I figured it was the simplest one I could think of that could use nested roots realizing there were other solutions. In generalizing the solution as a utility function in the same way SArray map methods, is the nested root approach reasonable? |
@mrjjwright I realize I may have prematurely led you down a rabbit hole in that other issue. I had been working so much on my own library I wasn't thinking about how you'd be likely using S-array. I can explain why you don't see S.root in the Surplus code and you do in the babel plugin. It's because at the point of With Surplus on update there are 3 steps.
The difference with the babel plugin is I combined step 2 and 3 into a single pass. The each binding is essentially S-array mapSample + Take a look at mapS and mapSample if you want another example of how the nested roots work. |
@ryansolid I notice that S-array is not a dependency (or peer dependency) for Surplus so I am assuming that a Surplus user must use mapSample in certain cases that are along the lines of this discussion and I see that this is indeed done by @adamhaile e.g. in the surplus-todomvc example here: https://github.com/adamhaile/surplus-todomvc/blob/master/src/views.tsx#L25 I also see that in the source for I also understand that you wanted to combine the passes through the array in your babel plugin, probably for performance and correctness reasons and then mimic what is done to some extent in mapSample. I also see that there is another case covered by your So I see the general outlines of what you are saying, thank you so much for the clear explanation and I will keep studying to understand more. I figured out my original problem. It's really the fundamentals that continue to trip me up, no big surprise since that is true with most anything in life. In my app, when the user presses a key, I set the The big takeaway for me is to continue to remember that dependent computations created in one Feel free to state it better if somebody sees any flaws in my understanding. @adamhaile and @ryansolid, again thank you so much. I am absolutely in love with this stuff and because both of your code is written so minimally and clearly (unlike the bigger libs of mobx/vue/rxjs), I hopefully have a chance at actually creating bug free code around this that still has great performance. |
Yeah, ok, that's a prime case of where
const
inits = SArray[]),
objects = inits.map(params => new SomeObject(params));
S.on(someEvent, () => inits.push(newObjectConstructorParams));
// ... later, when it's time to dispose it
inits.remove(newObjectConstructorParams);
const objects = SArray([]);
class SomeObject {
constructor(params, dispose) {
....
this._dispose = dispose;
objects.push(this);
}
dispose() {
this._dispose();
objects.remove(this);
}
}
S.on(someEvent, () => S.root(dispose =>
new SomeObject(params, dispose)
));
// ... later
object.dispose(); |
@adamhaile I did almost exactly # 2 of your solutions on my own yesterday but just hadn't understood why it was working and needed until this morning, when some comments by @ryansolid tripped my understanding. The first solution is super cool and since my |
If a computation computes new signals, i.e. new state e.g. by computing a new
S.data
signal, are there any rules of which to be aware? I haven't been successful in exactly understanding the rules. In a lot of cases it works, but in some rare cases I have to create a newS.root
and dispose that root manually in the cleanup method of the computation.Most of the high level state that drives the rest of the computation of an app can be created in the main S.root but how to handle cases where you need to create new signals?
The text was updated successfully, but these errors were encountered: