HelloZeroNet/ZeroNet · error · Error

The selector for the root VNode may not be changed. (conside

Error message

The selector for the root VNode may not be changed. (consider using dom.merge and add one extra level to the virtual DOM)

What it means

A projection returned by dom.append/merge/createDom caches its root vnode's selector; on update the new root vnode must keep the same vnodeSelector because the root DOM node itself cannot be replaced by the projector. Changing the root selector would require replacing the projection's root element, which maquette refuses.

Source

Thrown at plugins/UiConfig/media/js/all.js:673

                }
            }
            updated = updateChildren(vnode, domNode, previous.children, vnode.children, projectionOptions) || updated;
            updated = updateProperties(domNode, previous.properties, vnode.properties, projectionOptions) || updated;
            if (vnode.properties && vnode.properties.afterUpdate) {
                vnode.properties.afterUpdate(domNode, projectionOptions, vnode.vnodeSelector, vnode.properties, vnode.children);
            }
        }
        if (updated && vnode.properties && vnode.properties.updateAnimation) {
            vnode.properties.updateAnimation(domNode, vnode.properties, previous.properties);
        }
        vnode.domNode = previous.domNode;
        return textUpdated;
    };
    var createProjection = function (vnode, projectionOptions) {
        return {
            update: function (updatedVnode) {
                if (vnode.vnodeSelector !== updatedVnode.vnodeSelector) {
                    throw new Error('The selector for the root VNode may not be changed. (consider using dom.merge and add one extra level to the virtual DOM)');
                }
                updateDom(vnode, updatedVnode, projectionOptions);
                vnode = updatedVnode;
            },
            domNode: vnode.domNode
        };
    };
    ;
    // The other two parameters are not added here, because the Typescript compiler creates surrogate code for desctructuring 'children'.
    exports.h = function (selector) {
        var properties = arguments[1];
        if (typeof selector !== 'string') {
            throw new Error();
        }
        var childIndex = 1;
        if (properties && !properties.hasOwnProperty('vnodeSelector') && !Array.isArray(properties) && typeof properties === 'object') {
            childIndex = 2;
        } else {

View on GitHub (pinned to 454c0b2e7e)

Solutions

  1. Keep the root vnodeSelector constant across updates; change only its properties and children.
  2. If the root must change, wrap it: project a stable outer node (e.g. h('div')) and swap the inner child each render.
  3. Use dom.merge and add one extra level to the virtual DOM, as the error message suggests.
  4. Create a new projection (dom.append again) when the root genuinely changes, replacing the old one.

Example fix

// before
projection.update(state.tab === 'a' ? h('div.panelA') : h('div.panelB'));
// after
// stable root, variable child
projection.update(h('div.panel', [state.tab === 'a' ? renderA() : renderB()]));
Defensive patterns

Strategy: validation

Validate before calling

function assertSameRootSelector(prevVnode, nextVnode) {
  if (prevVnode && nextVnode && prevVnode.vnodeSelector !== nextVnode.vnodeSelector) {
    throw new Error('Root selector must stay constant: ' + prevVnode.vnodeSelector + ' -> ' + nextVnode.vnodeSelector);
  }
}
// call before projection.update

Type guard

function sameRootSelector(prev, next) {
  return prev == null || next == null || prev.vnodeSelector === next.vnodeSelector;
}

Try / catch

try {
  projection.update(next);
} catch (e) {
  if (e.message.indexOf('root VNode may not be changed') !== -1) {
    console.error('Keep root selector constant; vary children instead');
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling projection.update(updatedVnode) where updatedVnode.vnodeSelector differs from the original root selector (e.g. root was h('div.app') and the update supplies h('section.app')); also when conditional rendering swaps the root element type.

Common situations: Toggling between two different root tags based on state; refactoring the top-level component's root element; misusing a projection whose natural lifetime is tied to one root node.

Related errors


AI-assisted analysis of HelloZeroNet/ZeroNet@454c0b2e7e (2026-09-02). Data as JSON: /api/errors/469a7fba29bd6f4a. Report an issue: GitHub.