Understanding `createRoot`
There isn't a lot of accessible and straightforward information about how
createRoot
works, why it exists, and when it should be used. To my knowledge, it's a relatively advanced topic, and there is rarely a need to use it. I don't have a specific use case for it either, but I wanted to update the documentation, so I need to understand it better first. Specifically, I want to address this issue.
Here are my questions; please provide simple explanations:
1. Why is it named createRoot
? Root of what?
Creates a new non-tracked owner scope that doesn't auto-dispose. From the Solid docs.2. What is an owner scope? What is the difference between an owned scope and an unowned scope?
All Solid code should be wrapped in one of these top level as they ensure that all memory/computations are freed up. From the Solid docs.3. What are the sources of memory and computations that need to be freed up? What problems can arise if they are not freed? 4. Can you provide an example of how the
detachedOwner
prop could be useful?
5. Can you provide a practical example of using createRoot
with code?
Since this is an advanced topic and I want to ensure accuracy for the documentation, I'm tagging a few individuals who I believe might be knowledgeable about this to draw their attention. I typically do not do this. @ryansolid @devagr @Brendonovich @AtilaGitHub
Issues · solidjs/solid-docs
Official documentation for the Solid ecosystem. Contribute to solidjs/solid-docs development by creating an account on GitHub.
10 Replies
i can help here but i see ryan is typing, i'll let him finish
1. Root of the ownership tree/graph. People are generally aware of the dependency graph as they see the auto tracking "magic" but we also collect reactive primitives created inside for automatic disposal. This tree is the basis of our Context solution.
2. There are 2 globals we update as we run our reactivity, our Observer(Listener) and our Owner. When we untrack we remove the listener but always have the owner. Some scopes are untracked but they are always owned. An unowned scope would be creating a reactive primitive top level or in an event handler or setTimeout.
3. The observer pattern, what Signals are, are inherently leaky. They need to link both directions between the observer and the observed. This means if one is long lived the other will never automatically be released. Like if you had a
createMemo
that created another createMemo
under that listened to a global Signal. Every time the outer memo reruns it would create it again. If we didn't register the inner memo to be released when the outer reran because the global Signal is never going away it would just keep accumulating subscribers on every run.
4. This is pretty advanced, similar to runWithOwner. Maybe the name is poor. It is just a way to give the root an owner for Context purposes without registering it for automatic disposal.
5. Someone else can do this. It is for when you want to be able to manually dispose something. I can't think of any simple examples at the moment that aren't contrived. Originally ever Solid app started with a createRoot
call and you'd append the returned JSX to the DOM yourself. But now that code is wrapped in render
Thank you, Ryan, for your response.
It seems that the owner of the reactive scope—whatever it’s called—is responsible for managing the reactive scope and remains active. This means it doesn't get untracked or removed, as there would be no one to manage the reactive scope without it.
Is the main difference between an owned and an unowned reactive scope that the unowned reactive context, created by, for example,
createEffect
, can be removed?createMemo
and createEffect
create an owned scope. every reactive primitive will create an owned reactive scope. the unowned scopes will be the logic outside of components in the global scope, in event handlers, and anything async (like promise.then)
let's get a little more basic.
taking the example of rxjs. when you subscribe to something with rxjs, you get a subscription object back with a method to dispose the subscription
without owners, the equivalent in solid would look like this
this level of manual disposal is easy to forget to do, you'll see a bunch of articles around this topic in angular+rxjs land. Every subscription you create needs to be manually disposed.
Roots fix this by inverting control
Now everything created inside the root will dispose automatically when you dispose the root.
this is why you can just create reactive stuff inside components without every having to worry about unsubscribing/disposing like you do with other reactive systems like rxjs.
roots are first class mechanisms to track everything created inside a function scope and dispose them all together.
unowned
scope basically means that there is nothing keeping track of what you create, so if you create reactive stuff it will basically live foreverThank you, that was a good explanation. I think I understand it now.
you'd rarely need to use
createRoot
yourself, because it's done automatically under the hood of things like <Show
and <For
for example a <Show
component creates a root and renders the children inside it. If the when
condition changes, it will dispose that root, and create a new one to render the fallback.
if it changes again, it disposes the fallback root and creates a new one for the children.
<For
will create a root for each array item and dispose them if the item is removed from the arrayAnother question: the docs says that
createRoot
created an non-tracked owner scope. Why it's non-tracked?untracked as in it doesnt track signal reads like memos and effects. so
roots only track creation, not reads. effects/memos track both.
maybe
non-tracked
is not the most accurate terminologyAnother question: the docs says that
createRoot
frees memory and computations. Memory is understandable, but what creates computation and what it means to free a computation? Is it related to the createComputed
primitive?computation is an umbrella term for every signal/memo/effect/whatever else
you can register cleanups inside any owned scope using
onCleanup
. these are also called when the owner of the scope is disposed