Node decorations before custom node view causes rerender

I am running into problems with the interaction between a node decoration and a custom nodeView. When the node decoration is before the custom nodeview, and is then removed (i.e. replaced with an DecorationSet.empty or a DecorationSet that has no decorations), the entire document rerenders (and the node view is recreated).

In this case, my decoration adds a class to empty paragraphs (a prompt for users) (see github) and my node-view is an iframe (see github). The behaviour means the iframe gets rerendered on selection changes (causing flicking/loading, and other more complicated node views get recreated as well).

This seems to only happen when the prompt decoration is removed when it is before the iframe in the document, and when also involved in a selection change (it doesn’t happen when there are document changes). An example is below (also online here):

iframe-decoration-bug

I was wondering if a quick look at the decoration code below highlights the problem? Or if maybe this is a bug? Or expected behaviour? I have also tried using the DecorationSet to map/add/remove the decorations. When it gets down to an empty set, that always seems to trigger a full refresh and flicker in the IFrame/custom nodeView.

I also think this may have been introduced in the last four or so weeks, (this code did work before, but I am having trouble reproducing that and getting back to the right versions; our deployed app doesn’t do this). I can get a minimal example together to demonstrate this if it is helpful/worth it.

Decoration code

const promptPlugin = new Plugin({
  key,
  state: {
    init: () => DecorationSet.empty,
    apply(tr, value, oldState, newState) {
      if (!isEditable(newState)) return DecorationSet.empty;
      const paragraph = getParentIfParagraph(newState.selection);
      const emptyParagraph = paragraph && paragraph.node.nodeSize === 2;
      if (tr.selection.empty && emptyParagraph) {
        const deco = Decoration.node(tr.selection.from - 1, tr.selection.to + 1, {
          class: 'prompt',
        });
        return DecorationSet.create(tr.doc, [deco]);
      }
      return DecorationSet.empty;
    },
  },
  props: {
    decorations(state) {
      return this.getState(state);
    },
  },
});

I would try putting a console.log statement in the NodeView.update method for the iframe and seeing if update is called when that nearby prompt decoration is changed, just to double check whether its the decoration causing an update.

Thanks! Tried adding that: the update method is not called when the decoration is removed.

Removing the decoration plugin completely means the node-view is rendered as expected (only when the node-properties change). This also doesn’t happen if I leave the decorations in place and do not remove them.

Hi, your example link points to what appears to be a project page. Could you distill this problem down to a minimal script so I can debug it?

1 Like

Thank you! I will get a simple example one together over this weekend.

I am no longer seeing this issue in updated versions!

We hit a similar issue, although I’m not sure if it’s the exact same one or not - what we found is that if we have two blocks that are identical other than for a decoration on the first block, removing that decoration will cause all NodeViews after that point to be rebuilt, ie.

Block “A” | Decoration(foo) Block “B” … Block “A”

=>

Block “A” Block “B” … Block “A”

The reason appears to be that after the state change, the view reconciliation logic first looks for a block that is an exact match for both content + decorations - in this case the former NodeView for the second Block “A” is now a match for the first Block “A”, so it is selected as the replacement, and the preceding NodeViews are destroyed (to be subsequently regenerated).

1 Like

Do your node views have an update method? I can’t reproduce this issue in a simple setup that follows the situation you describe.

I have a test case at Glitch :・゚✧ - reproduces by clicking in the first (empty paragraph), and then click in the second paragraph block while watching the console logs - you should see that destroy() is called on 3 NodeViews with no preceding attempt at update()

(In this test case the regeneration is harmless of course, it’s more of a concern when the NodeView contents are expensive to rebuild (or do so asynchronously))

Thanks, I hadn’t entirely gleamed the structure of the case from your earlier description, but that script helped reproduce this. I think this patch should help here.

Yep that fixes it, thanks!

1 Like