Skip to content

This sequence of renders crashes in both V3 and V4 #103

@gbishop

Description

@gbishop

I'm monkey testing my app and have uncovered a sequence of renders that will crash both V3 and V4. I apologize for the complexity of this example; this is the shortest sequence of renders that I have found that causes it.

The error is

DOMException: Failed to execute 'setStartAfter' on 'Range': the given Node has no parent.
at range_default (http://localhost:5173/node_modules/.vite/deps/uhtml.js?v=3ae075b9:68:11)
at remove (http://localhost:5173/node_modules/.vite/deps/uhtml.js?v=3ae075b9:77:55)
at DocumentFragment.replaceWith (http://localhost:5173/node_modules/.vite/deps/uhtml.js?v=3ae075b9:106:5)
at Object.hole (http://localhost:5173/node_modules/.vite/deps/uhtml.js?v=3ae075b9:271:9)
at unroll (http://localhost:5173/node_modules/.vite/deps/uhtml.js?v=3ae075b9:425:21)
at unrollValues (http://localhost:5173/node_modules/.vite/deps/uhtml.js?v=3ae075b9:436:19)
at unroll (http://localhost:5173/node_modules/.vite/deps/uhtml.js?v=3ae075b9:411:18)
at unrollValues (http://localhost:5173/node_modules/.vite/deps/uhtml.js?v=3ae075b9:436:19)
at unrollValues (http://localhost:5173/node_modules/.vite/deps/uhtml.js?v=3ae075b9:438:7)
at unroll (http://localhost:5173/node_modules/.vite/deps/uhtml.js?v=3ae075b9:411:18)

start.js

import { html, render } from "uhtml";
import { data } from "./data.js";

/**
 * This tree is a very simplified version of my internal data structure
 * @typedef {Object} TreeBase
 * @property {string} className
 * @property {Object} props
 * @property {TreeBase[]} children
 * @property {string} id
 *
 * @typedef {import("uhtml").Hole} Hole
 */

/**
 * Wrap the code for a node into a component
 * @param {TreeBase} node
 * @param {Hole} body
 * @returns {Hole}
 */
function component(node, body) {
  return html`<div class=${node.className} id=${node.id} style=${""}>
    ${body}
  </div>`;
}

/**
 * A Stack has mulitple children
 * @param {TreeBase} node
 * @returns {Hole}
 */
function Stack(node) {
  return component(
    node,
    html`${node.children.map((child) => html`<div>${content(child)}</div>`)}`,
  );
}

/**
 * A Gap is simply a spacer
 * @param {TreeBase} node
 * @returns {Hole}
 */
function Gap(node) {
  return component(node, html`<div />`);
}

/**
 * Invoke the correct template for each node
 * @param {Object} node
 * @returns {Hole}
 */
function content(node) {
  if (node.className == "Gap") return Gap(node);
  else if (node.className == "Stack") return Stack(node);
  throw new Error("should not happen");
}

// cycle through the trees

let index = 0;
let step = 1;

function rep() {
  console.log({ index });
  try {
    render(document.body, content(data[index]));
  } catch (e) {
    console.error(e);
    clearInterval(timer);
  }
  index += step;
  index = index % data.length;
}
const timer = setInterval(rep, 100);

data.js: this defines the sequence of frames that cause the problem. Each array element is a tree.

export const data = [
  {
    // 9
    className: "Stack",
    children: [
      {
        className: "Stack",
        children: [
          {
            className: "Stack",
            children: [],
            id: "TreeBase-117",
          },
          {
            className: "Stack",
            children: [],
            id: "TreeBase-118",
          },
          {
            className: "Stack",
            children: [
              {
                className: "Stack",
                children: [],
                id: "TreeBase-122",
              },
            ],
            id: "TreeBase-121",
          },
        ],
        id: "TreeBase-5",
      },
    ],
    id: "TreeBase-4",
  },
  {
    // 8
    className: "Stack",
    children: [
      {
        className: "Stack",
        children: [
          {
            className: "Stack",
            children: [],
            id: "TreeBase-117",
          },
          {
            className: "Stack",
            children: [],
            id: "TreeBase-118",
          },
          {
            className: "Stack",
            children: [],
            id: "TreeBase-121",
          },
        ],
        id: "TreeBase-5",
      },
    ],
    id: "TreeBase-4",
  },
  {
    // 7
    className: "Stack",
    children: [
      {
        className: "Stack",
        children: [
          {
            className: "Stack",
            children: [
              {
                className: "Gap",
                children: [],
                id: "TreeBase-120",
              },
            ],
            id: "TreeBase-117",
          },
          {
            className: "Stack",
            children: [],
            id: "TreeBase-118",
          },
          {
            className: "Stack",
            children: [],
            id: "TreeBase-121",
          },
        ],
        id: "TreeBase-5",
      },
    ],
    id: "TreeBase-4",
  },
  {
    // 6
    className: "Stack",
    children: [
      {
        className: "Stack",
        children: [
          {
            className: "Stack",
            children: [
              {
                className: "Gap",
                children: [],
                id: "TreeBase-120",
              },
            ],
            id: "TreeBase-117",
          },
          {
            className: "Stack",
            children: [],
            id: "TreeBase-118",
          },
          {
            className: "Gap",
            children: [],
            id: "TreeBase-119",
          },
          {
            className: "Stack",
            children: [],
            id: "TreeBase-121",
          },
        ],
        id: "TreeBase-5",
      },
    ],
    id: "TreeBase-4",
  },
];

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions