Skip to content
This repository has been archived by the owner on Jul 28, 2023. It is now read-only.

Wrapperless hydration alternative: Directives hydration #60

Closed
luisherranz opened this issue Aug 26, 2022 · 11 comments
Closed

Wrapperless hydration alternative: Directives hydration #60

luisherranz opened this issue Aug 26, 2022 · 11 comments

Comments

@luisherranz
Copy link
Member

luisherranz commented Aug 26, 2022

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);

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 Provider may not work with one Provider per 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:

  1. Generate the static virtual DOM injecting the View components.
  2. 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.

@DAreRodz
Copy link
Collaborator

A simple way to do that would be to find the children of the View component and ignore the rest.

I did a quick example using <slot> tags to wrap children. The code comes from the preact-markup library, overwriting the visitor function to find the children nodes and moving them to their correct place.

https://codesandbox.io/s/preact-markup-interactive-blocks-using-children-wrapper-j576q0

Some caveats regarding DX would be these:

  • Developers should wrap children with <slot> in their block components instead of using children directly.
  • Developers would be in charge of fixing potential problems with CSS, using display: content in the <slot>, for example.

I haven't considered performance yet, but I guess that locating the children of a component and placing them in their correct position in the vDOM could be done while processing DOM nodes instead of doing that once the vDOM subtree has been generated.

Anyway, I'm sure there are other possible approaches we can explore and discuss (e.g. using an attribute in children nodes). Feel free to share feedback or more ideas. 😄

@luisherranz
Copy link
Member Author

luisherranz commented Aug 29, 2022

That's promising. Thank you, David 🙂


EDIT: We moved the children/slot conversation to #62

@yscik
Copy link
Collaborator

yscik commented Aug 29, 2022

That looks interesting!

One aspect that could get tricky is compatibility with existing plugins or scripts that work with the DOM on the page. Like an image carousel plugin for a simple example.

  • The vdom builder can pick up existing event handlers and pass them to React, but it'd probably need to attach them to the nodes natively.
  • If the script saves a reference to some node it'd break when its replaced
  • One option, probably costly, could be delaying the DOMContentLoaded event / document.readyState until the vdom is ready, which most scripts use to wait for the DOM
  • Also need to ensure that React never re-renders the static nodes

Another potential issue is interference between two separate plugins. For example, they could both add a global Provider with a redux store that's not namespaced. Most of this can probably be solved with documenting rules and expectations, since these plugins would be new developments, and also by providing safe APIs for common conflict points like the context provider example.

@yscik
Copy link
Collaborator

yscik commented Aug 29, 2022

EDIT: We moved the children/slot conversation to #62

@luisherranz
Copy link
Member Author

luisherranz commented Aug 30, 2022

EDIT: We moved the children/slot conversation to #62

@luisherranz
Copy link
Member Author

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.

I had a conversation with Preact's core team in their Slack regarding the use of Preact's internals that is relevant for this point. The summary is here:

@luisherranz
Copy link
Member Author

luisherranz commented Aug 31, 2022

One aspect that could get tricky is compatibility with existing plugins or scripts that work with the DOM on the page

@yscik: I don't think there is any significant change from what is already happening with the islands hydration.

I made a video because these types of things are not easy to explain with words. Please let me know if you think there's something else going on that I missed in terms of compatibility with existing scripts.

https://www.loom.com/share/8de5904c78984e2199fadb25c1ef0128

@yscik
Copy link
Collaborator

yscik commented Sep 1, 2022

Thanks for the explanation! I missed the fact that the static nodes won't be replaced at all because this is hydration. That resolves my concerns :)

For compatibility with external scripts, I was worried only about the static parts of the site, I wouldn't expect things not breaking when they meddle in the React components.

@luisherranz
Copy link
Member Author

Perfect then. Thanks, Peter!

@luisherranz
Copy link
Member Author

We've opened a new Tracking Issue to start working on this:

From now on, we'll publish updates on what we are working there, but we can keep using this issue for the conversation related to the pattern design.

@luisherranz
Copy link
Member Author

Closed as this was already implemented.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

3 participants