This document describes four pitfalls that affect RSG 2.0 SDK apps built with SolidJS, and the loop-closure limits of the SDK build. Each pitfall section explains the cause, how to identify the pitfall, and how to avoid it.

You can use this document to avoid unnecessary node recreation, warnings about unassigned nodes, render-thread exceptions, and unsupported loop constructs in your apps.

Pitfalls 2 and 3 apply only to RSG 2.0 SDK runtime 15.2 and lower; both were fixed in runtime 15.3, and each section repeats this note. You need those two sections only when supporting an app on runtime 15.2 or lower. Pitfalls 1 and 4 apply to every runtime version.

1. Recreation of nodes

When a component has a reactive dependency on a signal, SolidJS re-evaluates its props when that signal changes. If one of the props is a function that generates JSX, SolidJS calls the function again, which produces new object references for the JSX elements, even if their structure has not changed. As a result, SolidJS recreates JSX nodes unnecessarily.

The example app file pitfall-1.tsx demonstrates this pitfall.

To identify this pitfall, set the log level to TRACE to see node creation logs. When nodes are unnecessarily recreated, the logs show repeated createElement and removeNode messages for the same components.

Solutions

You can use either of the following options to avoid recreating the nodes. The example app file pitfall1-solution.tsx demonstrates the solution.

Option 1: Create the JSX once. You can create the JSX element once during initialization and reuse the same reference, rather than calling a function that creates new JSX each time.

Option 2: Use createMemo. You can wrap the JSX creation in createMemo so that the JSX is recreated only when its actual dependencies change.

You should not use createMemo when the JSX passes through a <Show>. Pitfall 3 below is the same createMemo used where the subtree can unmount; in that case, createMemo causes the problem. On runtime 15.2 and lower, you can use a plain reactive getter in that case. From runtime 15.3, either approach is safe.

2. Unassigned nodes in transaction warnings

When your code creates JSX elements but does not use them in a transaction (that is, in a render tick), the developer console shows warnings about unassigned nodes. The example app file pitfall-2.tsx demonstrates this pitfall.

To identify this pitfall, check the developer console for warnings about unassigned nodes.

This is a pitfall only in RSG 2.0 SDK runtime version 15.2 and lower. From runtime version 15.3, nodes can be created without being assigned to a parent within a transaction; therefore, this pitfall no longer applies.

3. Stale SceneGraph objects after unmount

When your code creates a JSX element (for example, a ContentNode) once, either with createMemo or with an immediately invoked function expression (IIFE), and then passes the element as a prop to a component inside a <Show>, the render thread destroys the underlying SceneGraph object when <Show> hides the component. However, SolidJS still holds a cached reference to the same (now-deleted) JSX element. When <Show> makes the component visible again, it attempts to assign the stale, deleted object, causing a render-thread exception: "Attempted to use a deleted/expired object".

The example app file pitfall-3.tsx demonstrates this pitfall.

To identify this pitfall, check the developer console for a render-thread exception that mentions a deleted or expired object, and that occurs after a component is hidden and then shown again.

This is a pitfall only in RSG 2.0 SDK runtime version 15.2 and lower. From runtime version 15.3, nodes created from the TypeScript side hold a strong reference in the JavaScript heap; therefore, you can safely reuse them across transactions, and this pitfall no longer applies.

Solution

You can use a plain reactive getter function (not createMemo or an IIFE) to produce the JSX. Each time <Show> mounts the component, the getter is called and returns a fresh JSX tree. SolidJS does not cache the reference across mount and unmount cycles; as a result, the render thread always receives a live object. The example app file pitfall3-solution.tsx demonstrates the solution.

This is the opposite of pitfall 1's Option 2. Caching the JSX avoids needless recreation, but it also keeps a reference alive after the render thread deletes the object. Which option to use depends on whether the subtree can unmount. On runtime 15.3 and later, the deletion does not happen; therefore, pitfall 1's advice applies without this exception.

The following example contrasts the two approaches:

// PITFALL: createMemo or an IIFE caches the reference; the deleted node is reused on remount
const content = createMemo(() => (
    <contentNode>...</contentNode>
));

// SOLUTION: a plain getter creates a fresh node on every mount
const content = () => (
    <contentNode>...</contentNode>
);

4. createTextNode and <Show> without fallback

Background

Roku SceneGraph does not have any concept of text nodes. The solid-js/universal renderer calls createTextNode("") internally to create positional placeholder nodes when dynamic content (for example, <Show>) evaluates to undefined. This happens when both of the following conditions are true:

  • The <Show> component has when={falsy} and does not have a fallback prop.
  • The <Show> component is a direct child of a real SceneGraph element (for example, <group>) with siblings.

The second condition matters because babel-preset-solid in universal mode compiles children of real elements using insert(parent, accessor, marker) where the next sibling becomes the marker. When <Show> returns undefined, the renderer calls cleanChildren which creates a placeholder via createTextNode(""). Fragments (<>) compile to arrays which use a different code path without markers; therefore, the issue does not occur there.

Solution

The createTextNode function creates an invisible group SceneGraph node as the positional placeholder. This is a zero-size, childless node that satisfies the renderer's marker requirements without affecting layout or rendering.

The following example works because a group placeholder is created when visible() is false:

// This works: the group placeholder is created when visible() is false
<group>
    <label text="Before" />
    <Show when={visible()}>
        <label text="Dynamic" />
    </Show>
    <label text="After" />
</group>

You do not need to provide a fallback prop to avoid crashes; the fallback prop remains optional.

Why the placeholder group cannot be shared or reused

Each createTextNode call returns a unique group node. A single shared node cannot be reused because:

  • Identity-based tracking: The solid-js/universal renderer tracks nodes by object identity (===). If two <Show> components returned the same placeholder, parent.c.indexOf(node) would always find the first occurrence, corrupting positional tracking for the second.
  • Single-parent constraint: A SceneGraph node can only belong to one parent. Inserting a shared node into a second parent silently detaches it from the first, breaking the first parent's child list.
  • Concurrent slots: Multiple <Show> components can be in their fallback state simultaneously. Each needs its own placeholder at its own position in the children array.

The per-call cost is one createNode call and one appendNode native IPC call, which is negligible compared to the hundreds of calls a typical screen transition makes for real content.

Loop closure capture and support limits

The SDK build rewrites loop closures so callbacks capture the iteration value. You should avoid labeled break or continue targets outside the immediate loop because the build does not transform those loops. If you need a closure in such a loop, you can pass the iteration value to a separate function that creates the callback.

Supported loop types

The following table lists the loop types that the build transforms to capture the iteration value:

Loop typeSupportedNotes
forYesFull support with initializer variable capture
for...inYesCaptures iteration variable
for...ofYesCaptures iteration variable, supports destructuring
for await...ofYesGenerates async hoisted function
whileYesCaptures variables defined in loop body
do...whileYesCaptures variables defined in loop body

Limitations

The following edge cases are not supported, and the build does not transform them:

Labeled statements

Loops containing break label or continue label that target labels outside the immediate loop are not transformed. This is due to the complexity of maintaining cross-scope label references when the loop body is hoisted.

The following example is not transformed because the labeled break targets the outer loop:

// NOT TRANSFORMED - labeled break targets outer loop
outer: for (let i = 0; i < 10; i++) {
    for (let j = 0; j < 10; j++) {
        if (condition) break outer; // Labeled break
        setTimeout(() => log(j), 100);
    }
}

Generator functions

Generator functions (function*) are not supported by the Hermes JavaScript engine; therefore, you cannot use them in Roku apps. For this reason, the transformer does not handle yield expressions inside loops. The following example is not supported:

// NOT SUPPORTED - Hermes does not support generator functions
function* generator() {
    for (let i = 0; i < 5; i++) {
        yield i;
        setTimeout(() => log(i), 100);
    }
}

Solid reactivity

SolidJS differs from other libraries (for example, React) mainly because it uses fine-grained reactivity and does not use a Virtual DOM (VDOM). The following list describes the differences that affect how you write components:

  • Components run only once: SolidJS runs a component once, during its initial render. Updates happen through reactive primitives (signals, stores, and so on).
  • Fine-grained reactivity: SolidJS uses a fine-grained reactivity model, where changes to individual reactive primitives update only the parts of the UI that depend on them.
  • No Virtual DOM: SolidJS does not use a VDOM. Instead, it compiles templates down to imperative code that directly manipulates the nodes.
  • Control flow constructs: SolidJS provides control flow constructs (for example, <For>, <Show>, <Switch>, and so on) that work with its reactivity model, which allows more efficient rendering of lists and conditional content.

Did this page help you?