In the previous article A Hands-on Introduction to Fine-Grained Reactivity I explain the concepts behind fine-grained reactivity through example. N...
For further actions, you may consider blocking this person and/or reporting abuse
I love this article! Why in Solid "batch" is a function you can import and use why no automatic baching, are there use-cases where you don't want to batch is that the reason?
Yeah I really wanted to have things execute synchronously originally. And there are cases where injecting batch ourselves I thought was taking control away from the end user. I've decided since that probably isn't that big of a deal. But I generally start from a place of control.
Loved this article. Thank you!
Hey Ryan,
I love this article, thank you for this!
You write that the algorithm you use here needs more work to prevent doing too doing too much work. Can you elaborate on that? I understood that each signal you update / set will set a chain in motion where every step will only trigger the next steps that are actually required. After each „chain reaction“ the whole dependency graph will be cleaned up and we‘re up to a fresh start. Sounds like no work can be done that should not be done, what am I missing, or getting wrong?
This is true until you move to updating multiple signals in the same transaction. Then things get more complicated because you have to start somewhere but if you notify and execute off one signal and one of the descendants also depends on the other that changed you'd end up running it twice with a naive approach. Not to mention if you allow derivations to conditionally notify, like only if their value changes, simple approaches don't work.
Ah, makes sense! Thank you!
Late reply but a simple example
This effect runs three times, not two - because update to a triggers update to b which triggers effect but also update to a itself triggers effect too
Hey Ryan,
Thanks for the blog post.
Q: What's the difference between
vs
createEffect(() => log("My name is", displayName()));
and without the
createMemo
, for my understanding, the subscription will be the same outcome, right?Cheers
Bill
You are correct. Good on you for noticing. I often have to teach people where they can use less primitives. People new to reactivity tend to overuse them, and it probably doesn't help that intro examples tend to be overly simplistic to illustrate mechanics rather than practical. And here I am guilty of that here.
In this scenario where there is only a single observer that has no other dependencies this will effectively execute the same number of times. I explain this and the example a bit more in my previous article A Hands-on Introduction to Fine-Grained Reactivity. I show an example in that article illustrating the difference between using a thunk or using
createMemo
in an example where it does make a difference.The main benefit of
createMemo
is to prevent re-calculations, especially expensive ones. This is only relevant if it is used in multiple places or the thing that listens to it could be triggered a different way which could also cause re-evaluation. It's just a caching mechanism, and like any memoization premature application actually can have negative impact since it costs extra memory and brings creation overhead. In real apps in any example where I was just calculating a string I'd probably not bother and just function wrap if there was reason not to inline it. But I'm also a bit of a performance nut.Aside: This is actually one of the reasons I personally like explicit reactivity. A lot of libraries/APIs sort of push this memoization on you by default or hide it in their templating. It's excellent for update performance but you end up with higher creation costs. A typical sign of this behavior is when they have APIs like
isObservable
etc.. as it means they make decisions based on a value being a primitive so wrapping in a thunk would not be sufficient.Thanks for the awesome and detailed reply. It does make sense. I really enjoy those reactive programming articles, hope to see more in the near future. ✌🏻
Very helpful article. One thing I would like to understand better is the lifecycle of effects, particularly when disposing of effects and signals that are no longer needed - it looks like the cleanup handler never gets called if you never set a signal.
I realize that in this simple example, effects aren't holding on to any resources, so everything would be garbage collected; but I'm having trouble seeing how you would extend this model to one where effects do need to be cleaned up.
My motivation, BTW, is I am working on a 3D game engine where character behavior is driven by a reactive framework that is similar to what you've outlined here. I'm in the process of refactoring it from a React hook style of reactivity to something more like Solid (with some important differences, such as reflection and type introspection).
Yeah this article doesn't actually get into anything hierarchical, or implement any sort of disposal. In Solid we have an idea of ownership where child effects are owned by parents as well and follow the same disposal pattern. If you want to understand this a bit more I recommend reading the article linked at the end, Reactivity to Rendering. I go into how I built a framework on top of this and it talks more about disposal and ownership.
Hi! Thank you for the great article!
Reading comments and your answers and then running code from the article i found that nested effects will create new subscription everytime outer effect runs, without cleaning up previous subscriptions, causing zombie inner subscriptions to appear. codesandbox.io/s/creatememo-forked...
You have mentioned nested effects two times:
Answering the question about why do we need array of contexts and not a single context
And the second time, answering the question about "why do we need clean-up in general"
Concidering these answers, i had come to conclusion that your code should already deal with that situation, since it has context stack and is, indeed, tearing up subscriptions, but it fact that does not help with nested effects. You have mentioned clean-up is needed to solve that, did you meant some mechanism that is not presented in the code of that article? In my understanding, some will need to store tree of contexts and dispose children subscriptions before re-executing current context to avoid zombie children.
So i am wondering if i missed out something or read something wrong.
Thank you!
Yes, an actual reactive system is more complicated. I'm missing cleanup on nested effects, but to be fair most reactive libraries do. Mobx and the like. They expect the user to handle final cleanup. Solid is special in that we build a hierarchical ownership graph to do this ourselves and this works really well with our rendering system, but it is far from the norm.
I guess showing a disposal API would make it simpler for this example, but it also distracts a bit since we don't use that in Solid. But implementing Solid's hierarchical system is also out of scope here.
I talk about Solid's ownership concept in my followup article Reactivity to Rendering.
In order to better make sense of the types, I tried porting this to TypeScript - I can't make sense of the type of the
dependencies
property of a subscription.Specifically, I can't make sense of these lines in the
reader
:Here,
subscriptions
is aSet
- but then you add the set ofsubscriptions
todependencies
, which is also aSet
(which gets created increateEffect
) soooo... is it a Set of Sets (of Subscriptions?) or what's going on here?Are you sure the code is correct?
TypeScript playground link
I mean I have a working example in the article. I'm basically backlinking the signal because on effect re-run we have to unsubscribe from all the signals that it appears in their subscription lists. So the link has to work both ways. The Signal here doesn't have an instance really and I just needed the subscriptions so I added that. I could have made it a callback maybe for clarity. This demo isn't how Solid actually works completely. It's just a way of doing the basics with next to no code.
Yeah, the example works - I guess I have trust issues with plain JS without types. I've seen too much JS that "works", but not for the right reason, or not the way it appeared to work, if you know what I mean?
I think types would better explain the data structures and concepts here - rather than making the reader reverse engineer the code to try to infer what's what. What's
running
, what's independencies
, and so on.It's challenging even after watching the video and reading the article.
I still can't make sense of the types.
Sometimes clarity comes from the chosen names.
Change #1:
Change #2:
Change #3:
Change #4:
TypeScipt Playground
Yes! This is what the article/video/example was missing. Thank you! 😀🙏
Hey Ryan, great article, I benefit a lot from it, thank you so much.
Q: Why use
context
stack?I replaced the
context
stack, and it words fine. full codeNested reactivity. Generally you can nest effects or derivations inside themselves. This is important to how Solid renders but I suppose wasn't needed for this simple example.
I nested effects, but it also works fine. full code
If you try to subscribe after the inner effect it doesn't track. Change the example to:
Keep in mind this is just a simple approximation. I don't actually use an array in my libraries and instead just store the out context and then restore it.
In the Reaction implementation, why is cleanup done before the next run and not after each run? (eg below)
You wouldn't want to cleanup yet. Like picture it is a DOM widget like a jQuery Chart.. you wouldn't cleanup until the end. The cleanup being run is the previously registered one. It goes something like this:
The dependencies need to remain so that if a signal happens it will trigger a reaction?
Yes that too. That's probably the more foundational answer. The reason we do cleanup each time is it lets our dependencies be dynamic. There are optimizations to be done around this but this is the core mechanism that you will find in MobX or Vue, along with Solid.
Hey Ryan, If I change
[...subscriptions]
tosubscriptions
. it will causes infinite loop.to
Error:
What is the cause? Thank you so much. Full Code
Yep. It's because the first thing each computation execution does is remove itself from the subscription list and then during the process of execution if it reads the same signal it adds itself back on. What this means is if we don't clone the original list, while iterating over it the process of executing the computation will cause it to end up at the end of the list. And it will then execute it again within the same loop, and add itself to the end of the list and so on.
Hi Ryan, Thanks for this post !
Why should the Effect itself hold a list of dependencies as well. The Signal holds a list of subscriptions that should be notified whenever the signal is modified, for example setCount(5), And that does the job right ? what am I missing ? In the code above effect's dependencies are being added but not really used anywhere except for cleaning itself but why do they exist in the first place).
In VueJS there is a hijacking of the state (data) itself and so at initialization the data properties are being replaced with reactive getters and setters (using defineProperty in the past and Proxy today) that notify listeners. In SolidJS is there hijacking as well ? or is it mostly 'pure functional' ?
Thanks !
Hey Ryan, I truly appreciate the very thoughtful architectural choices which shape Solid. Recently I've been switching from Virtual-DOM-based rendering to Solid for a rather complex project and it feels not only performant but so elegant! Keep up great work!
Thank you so much, I just started to learn
SolidJS
and it was confusing to understand howcreateEffect
detect changes of signals that used in the effect. Now, it's more clear then, thank you for this article!The observer pattern is inherently leaky. Signals don't need cleanup but any subscription does. If the signal outlives the subscriber it will have no longer relevant subscriptions. At minimum we need to mark the subscriber as dead. The fact that we dynamically create and tear down subscriptions on each run has us doing this work anyway. Consider this example:
Every time
a
changes you are creating a new reaction that listens tob
. So a naive approach that didn't do cleanup would just keep appending more subscriptions tob
. So if you updateda
3 times b would end up with 4 subscriptions. When you updatedb
it wouldconsole.log
4 times.Hey Ryan, thank you for the article series!
In the case of the demo you've built up in this article, the
b
wouldconsole.log
4 times. You writing "the fact that we dynamically create and tear down subscriptions on each run has us doing this work anyway," made me think it's not supposed to happen. But, it seems to make sense: each timea
changes, a new effect is created, and so theb
's will stack up.I've read that with solid the nested effect, "would be disposed on each rerun of the "parent" effect. Is this what happens in Solid, that doesn't happen in this article?
Thank you.