Render / replaceNode unexpected behavior?
See original GitHub issueI’m having some issues with render()
, followup to #2002
I have an app which is mostly not preact based, but uses a custom made system to generate HTML. We are migrating to preact, so different parts of the app are now created in preact. We currently use preact 8, and use this pattern:
const container = document.getElementById('someId');
let rendered = preact.render(<SomeComponent />, container);
// based on some events an update of the component may be required
preact.render(<SomeComponent />, container, rendered);
// base on another event it must be removed
preact.render("", container, component);
rendered = undefined;
This works like a charm.
Now I’m trying to migrate to Preact X and I’m facing some issues, because render changed. Maybe I do not understand the desired approach for this, but I found some strange effects using render()
:
https://codepen.io/rhinkamp/pen/abbdBgE The rendered component is not appended to container, but added before the h1.
https://codepen.io/rhinkamp/pen/ExxPNOv A existing div in the HTML is replaced by the component, but it’s position changed to before the h1.
https://codepen.io/rhinkamp/pen/WNNroVQ Using the replaceNode parameter it’s in the correct place, but the nodes look merged, since it’s the component and the id of the original node still present.
https://codepen.io/rhinkamp/pen/eYYJgGV If the replaceNode is a different type (span opposed to div) it is not merged?
https://codepen.io/rhinkamp/pen/gOOPLVJ A button to re-render the component with a different text suddenly drops the “Bar:” text on the first click. On the second click “Bar:” is back, but “allo” is missing.
https://codepen.io/rhinkamp/pen/XWWXpgE Using an empty container node and no replaceNode option does work correct.
https://codepen.io/rhinkamp/pen/OJJMWJL A button to remove the element and a button to add/update. This seems to work, but after removing and adding, clicking the add/update button again will remove all text from the component?
Are these cases correct but am I expecting the wrong thing, or are these bugs?
Is there a desired approach for adding preact component to an existing app? Especially adding it somewhere into the existing DOM tree.
I think I can create container elements to render components in, so I don’t have to use replaceNode, I guess that will fix most if not all issues, but the replaceNode does result in some unexpected behavior?
Issue Analytics
- State:
- Created 4 years ago
- Comments:13 (6 by maintainers)
Top GitHub Comments
Hi all,
Sorry for the radio silence on this one. I just came back to the issue and I’ve got a little guide here to help make sure everyone is on the same page. TLDR is: there is technically a bug here, but it exists in an undocumented feature we had in Preact 8.
Here’s a retrofitted/commented version of @yashrajpchavda’s great CodeSandbox demo with some additional explanations and an example of similar behavior we have today that does actually work in Preact X: https://codesandbox.io/s/preact-differing-root-subtree-render-bug-jywti
I’ll break it down here step-by-step though.
First, we’ll render a DOM tree using Preact:
Now, say we wanted to specifically update that
<a>
in-place, without having to re-render the whole tree again. In Preact 8 we diffed against the DOM, so it was possible to “pinpoint” an element in the DOM, and call render() directly on it with a new tree. This might have done some slightly strange things for Components, but for straight Element trees it “just worked” in 8 because we annotated the DOM with lots of stateful properties that were then used to render regardless of the root.What Does work
In Preact X we can also do this, with the similar constraint that DOM nodes associated with Components will end up with two Virtual DOM trees, which is never desirable. In general, this technique is treading the boundaries of what we can allow and I do worry that it’ll break in the future as we rely more on the Virtual DOM tree and less on the DOM tree for state.
Anyway, here’s the variant that works in both Preact 8 and Preact X - we find the link element in the DOM, and call render() on it with a modified description to apply:
This actually works fine aside from the concerns I mentioned above. It will trigger Preact’s non-hydration initial diffing algorithm, which attempts to reconcile VDOM against DOM (similar to Preact 8’s rendering).
What Doesn’t Work
Now, here’s the case that doesn’t work in Preact X, and that could potentially represent a bug that could manifest itself in other documented use-cases. If we want to do “pinpoint” rendering like this, but the new VDOM tree we pass to render() has a different root element type (eg:
<a>
vs<span>
), Preact X will not remove the previous DOM element we passed and will instead prepend the newly created tree before it. This is because render()'sreplaceNode
parameter was designed to allow diffing Preact trees within a parent that has other elements Preact shouldn’t touch. In X this is important because it’s fairly common to render multiple VDOM trees into the same parent element usingcreatePortal()
- each new tree needs to ignore other trees sharing that parent element.So here’s the version that fails in X but worked in 8:
The output after rendering the above will be this:
This is nearly correct, except that the original
<a>
we passed asreplaceNode
should have been removed since it was semantically replaced by our whole new tree.@richardhinkamp just a note - in your last demo, the
target
element reference is obtained once and held globally, but the button onclick actually removes that element from the page and replaces it with a Text node. That part is actually intended behavior, and the same as Preact 8. The reason this produces strange results is becausetarget
is passed again to the third render() call, but at that point it’s referring to an element that is not present in the DOM tree.