12

When working on a SolidJS project you might start seeing the following warning message in your JS console:

computations created outside a `createRoot` or `render` will never be disposed

There are some information available on this in SolidJS' Github repository issues. But after reading them I was still not quite sure what this was all about and whether my code was really doing something wrong.

I managed to track down where it came from and find a fix for it based on the documentation. So I'm providing the explanation and the solution for those Googling this warning message.

BenVida
  • 1,796
  • 1
  • 16
  • 25

2 Answers2

13

In essence this is a warning about a possibility of a memory leak due to a reactive computation being created without the proper context which would dispose of it when no longer needed.

A proper context is created a couple of different ways. Here are the ones I know about:

  • By using the render function.
  • By using the createRoot function. Under the hood render uses this.
  • By using the createContext function.

The first is by far the most common way, because each app has at least one render function call to get the whole show started.

So what makes the code go "out of context"?

Probably the most common way is via async calls. The context creation with its dependency tree happens only when the synchronous portion of the code finishes running. This includes all the export default function in your modules and the main app function.

But code that runs at a later time because of a setTimeout or by being in an async function will be outside of this context and any reactive computations created will not be tracked and might stick around without being garbage collected.

An example

Let's say you have a data input screen and have a Save button on it that makes an API call to your server to save the data. And you want to provide a feedback to the user whether the operation succeeded or not, with a nice HTML formatted message.

[msg,setMsg] = createSignal(<></>)

async function saveForm(){
  ...
  setMsg(<p>Saving your data.<i>Please stand by...</i></p>)
  const result=await callApi('updateUser',formData)
  if(result.ok){
    setMsg(<p>Your changes were <b>successfully</b> saved!</p> )
  } else {
    setMsg(<p>There was a problem saving your data! <br>Error: </p><pre>{result.error}</pre> )
  }
} 

...
  <div>
    ...
    <button onClick={saveForm} >Save</button>
    {msg()}
  </div>

This will produce the above mentioned warning when the API call returns an error, but not the other times. Why?

The reason for this is that SolidJS considers the code inserts inside JSX to be reactive, ie: need to be watched and re-evaluated. So inserting the error message from the API call creates a reactive computation.

The solution

I found the solution at the very end of the SolidJS doc. It's a special JSX modifier: /*@once*/

It can be used at the beginning of a curly brace expression and it tells the SolidJS compiler to explicitly not to make this a reactive expression. In other words: it will evaluated once and only once when the DOM nodes are created from the JSX.

In the above example here's how to use it:

setMsg(<p>There was a problem saving your data! <br>Error: </p><pre>{/*@once*/ result.error}</pre> )

After this there will be no more warning messages :)


In my case, I had an input and when that input changed I re-created an SVG drawing. Because the SVG creation was an expensive operation, I added a debounce in the createEffect function which ran when the input changed. debounce is a technique to defer the processing until the input stops changing for at least X amount of time. It involved running the SVG generation code inside the setTimeout function, thus being outside of the main context. Using the /*@once*/ modifier everywhere where I inserted an expression in the generated JSX has fixed the problem.

BenVida
  • 1,796
  • 1
  • 16
  • 25
  • 1
    Great answer. In my case, I replaced the `signal containing the JSX` with a `signal containing the data the JSX depended on`. ...in retrospect, that's what I should've been doing in the first place. SolidJS saving me from myself. – DharmaTurtle Oct 31 '22 at 13:42
3

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.

snnsnn
  • 10,486
  • 4
  • 39
  • 44