Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save developit/f4c67a2ede71dc2fab7f357f39cff28c to your computer and use it in GitHub Desktop.
Save developit/f4c67a2ede71dc2fab7f357f39cff28c to your computer and use it in GitHub Desktop.

preact-root-fragment: partial root rendering for Preact

This is a standalone Preact 10+ implementation of the deprecated replaceNode parameter from Preact 10.

It provides a way to render or hydrate a Preact tree using a subset of the children within the parent element passed to render():

<body>
  <div id="root">  ⬅ we pass this to render() as the parent DOM element...

    <script src="/etc.js"></script>

    <div class="app">  ⬅ ... but we want to use this tree, not the script
      <!-- ... -->
    </div>

  </div>
</body>

Why do I need this?

This is particularly useful for partial hydration, which often requires rendering multiple distinct Preact trees into the same parent DOM element. Imagine the scenario below - which elements would we pass to hydrate(jsx, parent) such that each widget's <section> would get hydrated without clobbering the others?

<div id="sidebar">
  <section id="widgetA"><h1>Widget A</h1></section>
  <section id="widgetB"><h1>Widget B</h1></section>
  <section id="widgetC"><h1>Widget C</h1></section>
</div>

Preact 10 provided a somewhat obscure third argument for render and hydrate called replaceNode, which could be used for the above case:

render(<A />, sidebar, widgetA); // render into <div id="sidebar">, but only look at <section id="widgetA">
render(<B />, sidebar, widgetB); // same, but only look at widgetB
render(<C />, sidebar, widgetC); // same, but only look at widgetC

While the replaceNode argument proved useful for handling scenarios like the above, it was limited to a single DOM element and could not accommodate Preact trees with multiple root elements. It also didn't handle updates well when multiple trees were mounted into the same parent DOM element, which turns out to be a key usage scenario.

Going forward, we're providing this functionality as a standalone library called preact-root-fragment.

How it works

preact-root-fragment provides a createRootFragment function:

createRootFragment(parent: Element, children: Node | Node[]);

Calling this function with a parent DOM element and one or more child elements returns a "Persistent Fragment". A persistent fragment is a fake DOM element, which pretends to contain the provided children while keeping them in their existing real parent element. It can be passed to render() or hydrate() instead of the parent argument.

Using the previous example, we can change the deprecated replaceNode usage out for createRootFragment:

import { createRootFragment } from 'preact-root-fragment';

render(<A />, createRootFragment(sidebar, widgetA));
render(<B />, createRootFragment(sidebar, widgetB));
render(<C />, createRootFragment(sidebar, widgetC));

Since we're creating separate "Persistent Fragment" parents to pass to each render() call, Preact will treat each as an independent Virtual DOM tree.

Multiple Root Elements

Unlike the replaceNode parameter from Preact 10, createRootFragment can accept an Array of children that will be used as the root elements when rendering. This is particularly useful when rendering a Virtual DOM tree that produces multiple root elements, such as a Fragment or an Array:

import { createRootFragment } from 'preact-root-fragment';
import { render } from 'preact';

function App() {
  return <>
    <h1>Example</h1>
    <p>Hello world!</p>
  </>;
}

// Use only the last two child elements within <body>:
const children = [].slice.call(document.body.children, -2);

render(<App />, createRootFragment(document.body, children));

Preact Version Support

This library works with Preact 10 and 11.

Changelog

0.2.0 (2022-03-04)

  • fix bug where nodes were appended instead of replaced (thanks @danielweck)
  • fix .__k assignment (thanks @danielweck)
  • fix Preact 10.6 debug error due to missing nodeType (thanks @danielweck)
{
"name": "preact-root-fragment",
"version": "0.2.0",
"main": "./preact-root-fragment.js",
"module": "./preact-root-fragment.js",
"type": "module"
}
/**
* A Preact 11+ implementation of the `replaceNode` parameter from Preact 10.
*
* This creates a "Persistent Fragment" (a fake DOM element) containing one or more
* DOM nodes, which can then be passed as the `parent` argument to Preact's `render()` method.
*/
export function createRootFragment(parent, replaceNode) {
replaceNode = [].concat(replaceNode);
var s = replaceNode[replaceNode.length-1].nextSibling;
function insert(c, r) { parent.insertBefore(c, r || s); }
return parent.__k = {
nodeType: 1,
parentNode: parent,
firstChild: replaceNode[0],
childNodes: replaceNode,
insertBefore: insert,
appendChild: insert,
removeChild: function(c) { parent.removeChild(c); }
};
}
@joyqi
Copy link

joyqi commented May 5, 2023

For those who are looking for the TypeScript version

import { ContainerNode } from "preact";
  
/**
 * A Preact 11+ implementation of the `replaceNode` parameter from Preact 10.
 *
 * This creates a "Persistent Fragment" (a fake DOM element) containing one or more
 * DOM nodes, which can then be passed as the `parent` argument to Preact's `render()` method.
 */
export function createRootFragment(parent: Node, replaceNode?: Node | Node[]): ContainerNode {
    if (replaceNode) {
        replaceNode = Array.isArray(replaceNode) ? replaceNode : [replaceNode];
    } else {
        replaceNode = [parent];
        parent = parent.parentNode as Node;
    }

    const s: Node | null = replaceNode[replaceNode.length - 1].nextSibling;

    const rootFragment: ContainerNode = {
        nodeType: 1,
        parentNode: parent as ParentNode,
        firstChild: replaceNode[0] as ChildNode,
        childNodes: replaceNode,
        insertBefore: (c, r) => {
            parent.insertBefore(c, r || s);
            return c;
        },
        appendChild: (c) => {
            parent.insertBefore(c, s);
            return c;
        },
        removeChild: function (c) {
            parent.removeChild(c);
            return c;
        },
    };
  
    (parent as any).__k = rootFragment;
    return rootFragment;
}

@sebastian-lenz
Copy link

This seems to no longer work with preact version 10.16.

@foxt
Copy link

foxt commented Oct 18, 2023

the JSDoc for the regular render(component, element) seems to lead to this page? Looking at the .d.ts makes it look to me as this was not intended?
image

@Julli4n
Copy link

Julli4n commented Jul 3, 2024

For anyone using Preact 10.22.1+, you will have to add a contains property to parent.__k:

/**
 * A Preact 11+ implementation of the `replaceNode` parameter from Preact 10.
 *
 * This creates a "Persistent Fragment" (a fake DOM element) containing one or more
 * DOM nodes, which can then be passed as the `parent` argument to Preact's `render()` method.
 */
export function createRootFragment(parent, replaceNode) {
  replaceNode = [].concat(replaceNode);
  var s = replaceNode[replaceNode.length-1].nextSibling;
  function insert(c, r) { parent.insertBefore(c, r || s); }
  return parent.__k = {
    nodeType: 1,
    parentNode: parent,
    firstChild: replaceNode[0],
    childNodes: replaceNode,
    insertBefore: insert,
    appendChild: insert,
    contains: function(c) { return parent.contains(c); },
    removeChild: function(c) { parent.removeChild(c); }
  };
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment