-
Notifications
You must be signed in to change notification settings - Fork 9
Wrapperless hydration alternative: Directives hydration #60
Description
I've been thinking about how to get rid of wrappers because the display: contents solution doesn't seem to be compatible with classic themes: #49.
One solution could be to iterate over the DOM and build a "static virtual DOM" of the full page. Something like this:
import { createElement, hydrate } from "react";
// convert a DOM node to a static virtual DOM node:
function toVdom(node) {
if (node.nodeType === 3) return node.data; // Text nodes --> strings
let props = {},
a = node.attributes; // attributes --> props
for (let i = 0; i < a.length; i++) props[a[i].name] = a[i].value;
return createElement(
node.localName,
props,
[].map.call(node.childNodes, toVdom)
); // recurse children
}
const vdom = toVdom(document.body);
hydrate(vdom, document.body);- This is the static virtual DOM generated using React's
createElement: https://codesandbox.io/s/react-vdom-from-document-body-itkew3?file=/src/index.js - This is the static virtual DOM generated by Preact's
h: https://codesandbox.io/s/preact-vdom-from-document-body-erz79j
As we iterate the DOM, we could check for the View components, and add them to the virtual DOM:
function toVdom(node) {
if (node.nodeType === 3) return node.data;
let props = {},
a = node.attributes;
for (let i = 0; i < a.length; i++) props[a[i].name] = a[i].value;
const type = props["wp-block-view"]
? blockViews.get(props["wp-block-view"]) // replace with View component
: node.localName;
return createElement(type, props, [].map.call(node.childNodes, convert));
}Although for this to work properly, we need to avoid adding the nodes that belong to the View component as static virtual nodes or they would appear duplicated. A simple way to do that would be to find the children of the View component and ignore the rest.
This approach could support hydration techniques. It could work by checking the value of a prop (i.e., wp-block-hydration) and creating static virtual nodes if the conditions are not met yet. Then, once the conditions are met, replace the static virtual nodes with the virtual nodes generated by the View component.
This method would have some advantages over the current one:
- It doesn't require wrappers.
- Things that currently require wiring between islands (context, suspense, error boundaries, etc.) would work out of the box. Also, there won't be related issues due to the wiring, nor inconsistencies due to the
setTimeouts. - But the main advantage that I see is: thanks to the virtual DOM, it'd be ready for client-side navigations that preserve the initialized components and minimize DOM manipulations. This is one of the goals of these experiments and is sometimes referred to as the "React Server Components pattern", although in this case, this would work with plain HTML.
- Full support for all React libraries. Libraries that require you to add a custom
Providermay not work with oneProviderper island, like for example Recoil.
And one disadvantage:
- The initial cost of turning the DOM into a static virtual DOM. I haven't yet explored how expensive it would be.
The preact-markup package is doing something similar to this approach. I've used it to emulate this, including the client-side navigations:
https://www.loom.com/share/35d882062e2447f3b2b9cae78cb75127
You can play with that codesanbox here: https://codesandbox.io/s/preact-markup-interactive-blocks-6knev6?file=/src/index.js
Credits to Jason Miller and preact-markup for a big part of the inspiration.
I would start trying this with Preact, and doing so in two steps:
- Generate the static virtual DOM injecting the View components.
- Hydrate the virtual DOM using Preact's
hydrate.
If it works, we could try doing the hydration during the generation of the static virtual DOM to optimize the performance because, at that moment, you already know the vNode <-> DOM node relation.