preact
preact copied to clipboard
Deliberate breaking change in render() ?
I noticed an important difference between render() in Preact 8 vs current beta 10.
With 8.4.2:
https://jsfiddle.net/mindplay/4Lebz2x1/1/
With 10.0.0-beta.2:
https://jsfiddle.net/mindplay/hxnr23jL/5/
As you can see, with the current beta 10, the rendering order is opposite - where Preact 8 would append to existing elements, Preact 10 seems to preprend before existing elements.
The practical use-case that broke here is a pop-up dialog being rendered over an existing page. Where, previously, this would append to the document body, now it's being prepended, and disappears under headers, background images, ads, etc.
We can control this with z-index of course, but this particular script needs to drop-in and work on different sites with different CSS - so my work-around for now is to emulate the old behavior by manually, e.g.:
render(<...>, document.body.appendChild(document.body.createElement("div"));
No big deal, though I now have an extra wrapper div and/or need to manually initialize the root element (classnames, etc.) of that dialog using DOM methods.
Is this a documented, "by design" change? (why?)
Appending a newly created node seemed more intuitive and practical to me. 🤷♂️
For the record, neither append or prepend is compatible with React, which destroys existing children:
https://jsfiddle.net/mindplay/woubtzf0/
This was listed as a breaking change in the release notes for the initial alpha.0. It's not like we made the decision to remove it, it's rather that we didn't add it.
In the 8.x release line the render root was always a single node and we just called appendChild with it. In X this assumption is no more as we now do support Fragments. This means the root can render multiple DOM siblings at once.
// Preact 8.x
render(
<div>...</div>,
dom,
);
// Preact X
render(
<>
<div>A</div>
<div>B</div>
</>,
dom
);
In that scenario the question where we should insert that Fragment isn't as straightforward to answer. Some expect it to be in the beginning, some right after a specific node and some expect it to be inserted at the end.
We're currently facing a similar issues with Portals where some users point it to the same container as the root render() call, see #1713 .
Some expect it to be in the beginning, some right after a specific node and some expect it to be inserted at the end.
Isn't it better then to err on the side of compatibility and keep it the same as before?
With regards to consistency, I'd expect appending a fragment to work the same as appending anything else - appending, in other words. (I don't see how appending a fragment is very different from appending a root element? Seems reasonable to expect it to append either way...)
Isn't it better then to err on the side of compatibility and keep it the same as before?
In my opinion, it is better to err on the side of compatibility. But with React, not preact 8.x. If react smokes the children, there's probably history there. I would not diverge from react compatibility by accident, only in cases with strongly supported argument.
I've never used React, so compatibility with React isn't really a good argument from my point of view.
React expects an empty render target - Preact (8) of course allows rendering into an empty target, so it is compatible with React in that sense.
Having the option to render to a target with existing children, from my perspective, is an extra feature - not an "incompatibility". It's really convenient when adding dialogs or other interactive content to pages with existing content.
Either way, Preact X should be compatible either with 8.x or React - introducing a third behavior isn't really meaningful, I think?
My strong preference would be BC with Preact 8.x, as I have a lot of projects based on that, and frequently rely on rendering interactive things into pages with content.
I have the exact same issue.
I even tried the following:
let node = /* the node where I want my mounted element to appear */;
let parent = node.parentElement;
let after = node.nextElementSibling;
let props = {
ref: component => parent.insertBefore(component.base, after)
};
render(<MyComp {...props} />, parent);
But apparently there is a race condition or some other issue, because it doesn't work.
If I dump the node and call it after some time it works (so maybe the DOM isn't flushed yet when calling ref, I don't know).
Any direction? Having redundant <div></div> littered through my app feels pretty unnecessary and bloats the DOM.
I'm pretty confused about how this works. The documentation I was reading suggested the old version of render where this would append to the container, which is what I want. But when I do this:
render(<MyComponent />, container)
The result is:
<div class="container">
<div class="my-component" />
<div class="existing child" />
</div>
I've tried appending by using the third replaceElement argument:
render(<MyComponent />, container, container.appendChild(document.createElement('div')))
But the result of that is:
<div class="container">
<div class="existing child" />
<div class="my-component" />
<div />
</div>
The element which is supposed to be replaced doesn't get replaced at all.
Even more bizarre, if I start with a container like this;
<div class="container">
<div class="existing child" />
<div class="target">existing text</div>
</div>
And then do:
render(<MyComponent />, container, container.querySelector('.target'));
The result is:
<div class="container">
<div class="existing child" />
<div class="target">
existing text
<div class="my-component" />
</div>
</div>
I can get the desired result, of appending my component if I do this:
render(<MyComponent />, container, container);
Which will result in:
<div class="container">
<div class="existing child" />
<div class="my-component" />
</div>
But that doesn't seem to match the docs. Is this behavior intentional, or is it a bug?
Hi @localjo 👋
What doc page were you looking at that suggested render appended to the container? I quickly glanced through a couple pages and couldn't find what you were referring to.
Also, what version of Preact are you using? Are you using preact/compat? If so, what aliases do you have setup?
I tried to repro the examples you provided above and couldn't 😕 https://codesandbox.io/s/1718-2iiyo
Could you send us a codesandbox (or some other runnable example) that shows the behavior you are seeing? It would go a long way to helping us dig further!
Your examples look right... but I'm definitely not seeing that behavior in my code. I'm using [email protected] with [email protected] and [email protected]. I can't figure out what might be different between my code and the reduced samples I shared, so if you want, you can check out my codebase. Here's where I'm using render: https://github.com/localjo/quotable-toolbar/blob/3bead9f64f2fcf54e9c811c23f79299ede9f4eb4/src/index.tsx#L122-L130
The codebase is a WIP, but you should be able to run the sample page if you clone, check out that commit, install dependencies, and run yarn start.
+1. Also seeing the behavior localjo is describing
// Get the parent
const parent = document.getElementById('app');
// Create and append target so we can replace it
const target = document.createComment('I have a target on my back');
parent.appendChild(target);
// Render Component, replacing target
render(<Component />, parent, target);
Result:
<div id="app">
<div class="Component"></div>
<!-- I have a target on my back -->
</div>
I solved it by removing target after the render():
// Render Component, replacing target
render(<Component />, parent, target);
// Remove the target because preact prepends Component, leaves target in the DOM.
target.remove();
Maybe the docs and especially the replaceNode argument name are a bit misleading? It says it uses replaceNode to determine where to start rendering, not that it actually replaces anything:
If the optional replaceNode parameter is provided, it must be a child of containerNode. Instead of inferring where to start rendering, Preact will update or replace the passed element using its diffing algorithm.
https://preactjs.com/guide/v10/api-reference/#render
Or is it possible that Preact doesn't touch target because it's not part of its virtual DOM?
Preact 10.6.4 Preact-cli 3.3.3