-
-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add a way to hydrate and render a Preact app in a defined region of the document head #3285
Comments
Normally the insertions of analytics scripts should be preserved (if they're deterministic) https://codesandbox.io/s/hardcore-meitner-yzowb?file=/src/index.js:752-773 here we preserve all existing tags but only render in 2 new ones 😅 Will look into what effects |
I've had some breakthroughs! I was making some incorrect assumptions that was leading to most of the issues. The strategy is to hydrate the body app first, so all the declared head content is discovered via I didn't realize (I could swear I experimented for this but mustn't have done so correctly) that after the Preact So what was happening, is I was hydrating the head app before all the head content had been declared to the head manager instance via the body app So the fix is to put a One strange thing though, is that the above fixed approach has a strange bug. None of the head app Lines 156 to 165 in bd52611
My question to the Preact team/community; is it safe to render two separate Preact apps that use hooks in the same browser window at overlapping times? If not, is it a bug, can it be made to be? |
Also, my original concerns about DOM node equality checks in Preact and the proxy of |
@jaydenseric you can achieve this without the proxy by creating a fake DOM element to pass to Preact's render() or hydrate() methods: // A fake DOM element we pass to Preact as the render root that exposes/mutates a subsequence of children.
class PersistentFragment {
constructor(parentNode, childNodes, nextSibling) {
this.parentNode = parentNode;
this.childNodes = childNodes;
this.nextSibling = nextSibling;
}
insertBefore(child, before) {
this.parentNode.insertBefore(child, before || this.nextSibling);
}
appendChild(child) {
this.insertBefore(child);
}
removeChild(child) {
this.parentNode.removeChild(child);
}
}
// Usage:
const children = [];
const end = document.head.querySelector('[name="managed-head-end"]'); // can be omitted if last!
let node = document.head.querySelector('[name="managed-head-start"]');
while ((node = node.nextElementSibling) && node !== end) children.push(node);
// construct the fake root to hydrate only the given Array of children:
const fakeRoot = new PersistentFragment(document.head, children, end);
hydrate(<HeadStuff />, fakeRoot); A variant of this that provides subsetted Regarding hooks/useEffect: it's safe to run multiple distinct Preact apps on the same page, they will all use the same global scheduler. The bug you ran into is #2798, and your solution is the correct one - invoking render() synchronously within a useEffect() callback resets the global scheduler while it is being flushed. It's a bug, but rather than solve it directly in Preact 10, we're looking to fix it Preact 11 via the createRoot API, which creates scheduler sub-queues for each root. |
Also - as an added bonus, import { createPortal } from 'preact/compat';
const fakeRoot = new PersistentFragment(document.head, children, end); // as above
function App() {
return (
<div>
{createPortal(<HeadStuff />, fakeRoot)}
</div>
);
}
render(<App />, document.body); |
I've been working hard on this problem again, and am currently stuck due to a Preact rendering bug (#2783). The managed head tags are a Here is a demonstration of the unnecessary remounting of DOM nodes and the FUOC it causes: Screen.Recording.2021-12-13.at.7.18.59.pm.movHere you can see how just by moving the Screen.Recording.2021-12-13.at.7.20.08.pm.movTrying to workaround the issue by manually ordering things isn't viable, because any of the managed head tags are supposed to be able to change. There is no safe order. |
For Ruck (the buildless React web application framework for Deno) I ended up having to abandon Preact, due to #2783 (comment) and also because of types conflicting with React's used by dependencies. Once Preact v11 is mature I’ll reconsider supporting Preact. Here is the published
Here is where it is used for Ruck app hydration in the browser after SSR: https://github.com/jaydenseric/ruck/blob/v5.0.0/hydrate.mjs#L43 Due to the different way React walks the DOM it had to be a little more complicated than the Preact implementation. It might be possible to support both React and Preact, but it would be a bit wasteful to have excess code in the implementation for the framework not being used so perhaps we would then be better off with seperate React and Preact functions. That would then require a way to specify the right function in a Ruck app depending if the author is using React or Preact (via import maps, or a You can see example Ruck apps here: https://github.com/jaydenseric/ruck#examples It's a thing of beauty to click around the routes (e.g. https://ruck.tech to https://ruck.tech/releases via the header nav link) with the browser inspector open to the document |
@jaydenseric also see Jason's other Gist: https://gist.github.com/developit/f4c67a2ede71dc2fab7f357f39cff28c |
Describe the feature you'd love to see
A way to hydrate and render a Preact app in a defined region of the document head.
Additional context (optional)
Using the entire
document.head
as the Preact app root is not viable, as often analytic scripts, etc. insert themselves or modify the contents of the document head and this would corrupt the Preact hydration and rendering. Also, an isomorphic / SSR web app framework should be able to offer users a way to statically template some of the head tags, while allowing others to be managed dynamically via component rendering side-effects.A way to hydrate and render a Preact app in a defined region of the document head would be a game-changer for head tag management, as then you could have a Preact app that hydrates and renders the head tags, and another Preact app that hydrates and renders the body HTML. The two apps can hold the same head manager instance in context, to coordinate head tag state updates in response to body component rendering side-effects.
I have 99% of such a system working, but Preact internals need to be slightly modified in order to get it over the line.
The challenge is of course, that the document head doesn't allow nesting DOM nodes under a container node like you can easily do with
<div>
in the document body. After trying a lot of ideas, the current strategy is to create a virtual DOM node that acts like a parent node for a real DOM node’s child nodes that are between a start and end DOM node:With HTML like this:
Note that in this example I'm using
meta
tags for the start and end DOM nodes, but you could use text nodes (e.g.<!-- managed-head-start -->
or any other uniquely identifiable DOM nodes.You can then create a new virtual DOM node to act as the head Preact app root:
And use it to hydrate the head Preact app:
This problem with this system is that sometimes Preact internally checks if DOM nodes are strictly equal. Here are some locations such checks exist:
preact/src/diff/children.js
Line 195 in bd52611
preact/src/diff/children.js
Line 303 in bd52611
preact/src/diff/children.js
Line 306 in bd52611
While our
headAppRoot
virtual node is a proxy of the realdocument.head
and should be functionally equal to it, these strict equality checks using!==
will result in Preact thinking they are not the same. This manifests in the initial hydration after SSR looking ok, all the head tags are adopted at first render, but any following renders due to state changes etc. result in the managed head tags being duplicated. From that point on, the duplicated head tags render in place from state updates etc. ok, but the original SSR tags permanently remain abandoned above.To deal with this, DOM node equality checks in Preact could be updated like this:
Using
.valueOf()
on a real DOM node likedocument.head
is perfectly safe; it just returns itself. The beauty is, this allows proxies of DOM nodes (our virtual node) to expose the underlying read DOM node it proxies for use in strict equality checks (see thecase "valueOf"
in thecreateVirtualNode
implementation show above).I have tried creating a custom build of Preact with
?.valueOf()
inserted at the three locations I could find where there are strict equality checks of DOM nodes, but it seems I don't understand Preact well enough to find all the places, as my modifications aren't solving the duplication issues on re-render. If anyone can identify what I'm missing, please share! I'm desperate.I feel like the massive amount of time (weeks) I've been spending on userland solutions working with the current Preact API is way less productive than the Preact team coming up with an official solution.
It would be rad if Preact would either offer an official
createVirtualNode
function orVirtualNode
class that can be used as the app root forhydrate
orrender
, or provide new hydration and render function signatures that accept arguments for start and end DOM nodes to define the app root as the slot between.The text was updated successfully, but these errors were encountered: