The error "Computations created outside a root" is emitted when you execute a computation outside a tracking scope.
What is a computation? Any form of effect that can subscribe to a signal, including the ones that are created via createComputation
, createEffect
, createRenderEffect
, createComponent
and createMemo
functions. Solid components are also effects.
What is a tracking scope? Tracking scope is a JavaScript scope that has access to an owner. If getOwner
function returns a value, you are inside a tracking scope. There are several ways to create a tracking scope but the most basic one is createRoot
, others like render
or createContext
calls it internally.
Why do we need a tracking scope? For memory management. A tracking scope tracks an effect's dependencies. Think of a component, a component can create a DOM element, and it have child components that can create other DOM elements. It is not only the components but even regular effects can host other effects inside its body.
If an effect listens for a signal, it will re-run. When they re-run they will repeat whatever they do. If it is creating a component, it will create new component. Effect hosting other effects that host other effects, may consume large amount of resource. If their consumption is not managed, it will get out of hand quickly.
When an effect is created under a tracking scope, Solid assigns an owner for for it, and builds a graph that shows who owns whom. Whenever an owner goes out of scope any computation owned by that owner gets disposed.
Tracking scope tracks internal resources, resources created by SolidJS itself. For external resources like socket connection you need release them manually via onCleanup
hooks.
The effect may have access to a signal or not is irrelevant. This dependency tracking exist outside a signal. Try running any effect that has no signal access, you will get the error all the same:
import { createEffect, createSignal } from 'solid-js';
createEffect(() => console.log('Hello World'));
You will receive this error if you execute an effect inside an async function even if the async function lives under a tracking scope. Why? Because Solid run synchronously. It runs in cycles. Effects subscribe to a signal when they react its value and unsubscribe once they are called back. So, everything is build up and tear down in each update cycle. When the async function runs, the owner of the previous cycle will be discarded long ago. So, the effect that lives inside an async function will be detached from the dependency graph and go rogue. But solution is simple: Providing an new owner by wrapping the effect with runWithOwner
function:
runWithOwner(outerOwner, () => {
createEffect(() => {
console.log('Hello World');
});
})
For other cases where you do not have a root scope, it is best to use render
or createRoot
functions.
Now it is time to explain how @once
pragma solves the problem inside the accepted answer:
First and foremost, you are creating a component inside the callback function by invoking the setMsg
.
The @once
pragma marks a prop value as static value.
Take this component:
<Comp count={count()} />
NORMALLY, the count prop is compiled to a getter function that returns the value:
_$insert(_el$3, _$createComponent(Comp, {
get count() {
return count();
}
}));
This is to preserve the reactivity when passing values from parent to child.
When @once
added, the prop's value will be treat as a static value:
_$insert(_el$3, _$createComponent(Comp, {
count: count()
}));
Remember we said components are effects. When @once
used, Solid treats the children as static values, not as components. In other words Solid does not see any effect inside the async function, but a function invocation that returns a static value:
<pre>{/*@once*/ result.error}</pre>
By the way, the example code that is used inside the accepted answer is not an idiomatic Solid component. It is best not to mix UI and state like that.