5

The problem

I'm trying to find a way to implement Islands architecture, with React 18.

The page I'm experimenting with looks like this

import React from 'react'
import { Counter } from './Counter'

export { Page }

function Page() {
  return (
    <>
      <h1>Welcome</h1>
      This page is:
      <ul>
        <li>Rendered to HTML.</li>
        <li>
          Non-Interactive <Counter />
          Non-Interactive <Counter />
          Non-Interactive <Counter />
        </li>
        <li>
          Interactive0. <Counter island="Counter:0" /><br/>
          Interactive1. <Counter island="Counter:1" /><br/>
          Interactive2. <Counter island="Counter:2" /><br/>
        </li>
      </ul>
    </>
  )
}

The latter three are marked with the island="" prop, indicating they are islands that needs to be hydrated.

What I've tried so far

The way I originally approached this task is

  1. Find a island
  2. Copy its siblings
  3. Hydrate the island
  4. Put the siblings back

Following this idea, I wrote the following client side code.

const tags = document.querySelectorAll("[island]");

for (let tag of tags) {
    const nodelist_siblings_rear = [];
    const nodelist_siblings_back = [];
    let isRearSibling = true;
    for (let sibling of tag.parentNode.childNodes) {
        if (sibling.isSameNode(tag)) {
            isRearSibling = false;
            continue;
        }
        if (isRearSibling) {
            nodelist_siblings_rear.push(sibling);
            continue;
        }
        nodelist_siblings_back.push(sibling);
    }
    const node_parent = tag.parentNode;
    const attributes = tag.attributes;

    // Force React to update the dom before proceeding.
    flushSync(() => {
        const parsed_attr = {};
        for (let item of attributes) {
            // console.log(item);
            parsed_attr[item.name] = item.value;
        }
        hydrateRoot(node_parent, <Counter {...parsed_attr} />);
        console.log(node_parent);
    });

    const node_new =
        node_parent.childNodes[0].nodeType === 3
            ? node_parent.childNodes[1]
            : node_parent.childNodes[0];

    for (let sibling of nodelist_siblings_rear) {
        node_parent.insertBefore(sibling, node_new);
    }

    for (let sibling of nodelist_siblings_back) {
        node_parent.appendChild(sibling);
    }
}

In this proof of concept, the <Counter /> component worked as intended, but it also said in the console, that the hydration failed.

Hydration failed because the initial UI does not match what was rendered on the server.

EDIT: Also, this method caused React to complain about createRoot() being called multiple times on the same DOM element.

Warning: You are calling ReactDOMClient.createRoot() on a container that has already been passed to createRoot() before. Instead, call root.render() on the existing root instead if you want to update it.

So I came up with other ideas, including

  1. Creating root and rendering in a DocumentFragment, and then move the child back to the DOM. This results in the component being successfully rendered but unable to react to clicks.
  2. Creating root on the parent, and somehow hydrate the whole parent and potentially all the sibling islands. I haven't tried this idea, because it might also hydrate components under the same parent, that are not islands.

Question

  1. Why did the first one failed? Is there any potential issue I can look into to make it work?
  2. Is there any other solutions I haven't think of, that I can look into?

Edit 1

The mismatch seems to be caused by server rendering

<button type="button" island="Counter:0">
    Counter
    <!---->
    0
<button/>

But after hydration on client side

<button type="button" island="Counter:0">
    Counter
    0
<button/>

That's the obvious part, I'm not sure if this is the root cause or maybe there's somethings else contributing to this error.

Chen Chang
  • 109
  • 1
  • 8
  • Posting this in case it is relevant - https://stackoverflow.com/questions/71706064/react-18-hydration-failed-because-the-initial-ui-does-not-match-what-was-render – sashoalm Feb 20 '23 at 21:46
  • I looked into it, however it seem like in my case, the mismatch is caused by the component's children, which I added to the edit section of the question. I played around with the placement of the ``, but the error is still present. – Chen Chang Feb 21 '23 at 05:09
  • I guess the mismatch issue can be solved by 2-pass rendering, one time on server-side, and a second one on the client-side for each islands. But that doesn't solve the problem of having multiple islands under the same parent. – Chen Chang Feb 21 '23 at 05:38

0 Answers0