Create a Preact component from an existing DOM element

  • preact
  • javascript
posted on 30th January 2023 ∙ by Hynek Svacha ∙ 4 min read

How to clone an existing DOM element (e.g., an image of a kitten with a caption) into the Preact component tree? Although it’s probably not such a common use case, it can be pretty useful to know.

There are two basic ways I know of. The first one is extremely simple, and it works in Preact the same way as it does in React:

Use dangerouslySetInnerHTML

// somewhere, probably in effect:
let element = document.getElementById("myElement");

// later in the component
<div dangerouslySetInnerHTML={{ __html: element ? element.outerHTML : null }}></div>

The real catch here is probably only one: If we want to clone the whole target element, we need to use its outerHTML instead of innerHTML however counter-intuitive it is due to the prop name (we are setting the div's innerHTML, not the element’s).

Use h() (createElement()) method

The second way is to use Preact.h() method (which is shorthand for Preact.createElement()). It is definitely more complex, but it gives us almost complete control over what’s going to be attached to our virtual DOM. We can sanitize the input, alter the attributes, assign refs, or do whatever else we want.

There isn’t currently any api that would just take an existing DOM element and convert it to a virtual DOM element (there is cloneElement() method, but it’s only for cloning elements that are already part of the virtual DOM).

However, we can take an existing DOM Element and create its copy in the shape that Preact’s h() would accept. If we peek at the docs, we’ll learn that the expected shape is h(type, props, ...children).

The type parameter is rather simple—it’s the name of the HTML element (e.g., h1, code).

For the purpose of cloning DOM elements, the props parameter can be thought of as an object containing the DOM element’s attributes (e.g., id, class, width).

How can we get those attributes? Unfortunately, there is currently no simple, self explanatory DOM api. There are Element.attributes, Element.getAttributeNames() and Element.getAttribute(), but none of them will give us an object with all DOM Element attributes right off the bat. But we can leverage those apis in a utility function:

const getAttributes = (element) => {
  const attributes = {};
  // fair warning: for..of loop is the only way 
  // how can we iterate over `Element.attributes`
  for (let attr of element.attributes) {
    attributes[attr.name] = attr.value;
  }
  return attributes;
};

If we’d need to clone a single childless element, like the <img /> element, we’d be done now. However, it would be much better to have a more flexible API capable of cloning a whole DOM branch!

So what’s the ...children parameter about? It accepts a simple virtual DOM tree of the same shape h() expects (or a string or array of strings in case the only children are text). We can create a function that will convert our browser DOM to Preact’s virtual DOM recursively:

import { h } from 'preact';

const toVNodeTree = (childNodes) => {
  const tree = [];
  childNodes.forEach((node) => {
    // Text node
    if (node.nodeType === 3) {
      return tree.push(node.data);
    }
    // Element node
    if (node.nodeType === 1) {
      return tree.push(
        h(node.nodeName, getAttributes(node), toVNodeTree(node.childNodes))
      );
    }
  });
  return tree;
};

If we’d like to sanitize, attach classes, other attributes of refs, pad the text nodes, or so, we’d be able to do it inside this function.

Putting it together

Put together, our code could look like this simple example:

import { h } from 'preact';
import { useState, useEffect } from 'preact/hooks';

const getAttributes = (element) => {
  const attributes = {};
  for (let attr of element.attributes) {
    attributes[attr.name] = attr.value;
  }
  return attributes;
};

const toVNodeTree = (childNodes) => {
  const tree = [];
  childNodes.forEach((node) => {
    if (node.nodeType === 3) {
      return tree.push(node.data);
    }
    if (node.nodeType === 1) {
      return tree.push(
        h(node.nodeName, getAttributes(node), toVNodeTree(node.childNodes))
      );
    }
  });
  return tree;
};

export const App = () => {
  const [element, setElement] = useState();

  useEffect(() => {
    const element = document.querySelector('#clone-me');
    if (element) {
      setElement(element);
    }
  });

  return (
      <div>
        {element
          ? h(
              element.nodeName,
              getAttributes(element),
              toVNodeTree(element.childNodes)
            )
          : null}
      </div>
  );
};

What about React?

React has a very similar api, React.createElement(). It works basically the same way, but unlike Preact, which is aiming to stay very close to the platform, React has diverged from it quite a bit.

There is no class, but className, there is no for but htmlFor and so on (the list would be rather long). It would also choke if the style attribute contained a string, rather than a style object.

There are some libraries that are aiming to convert standard DOM properties to the ones React accepts (e.g., react-attr-converter) or style-to-object (they are being downloaded quite a bit, despite being unmaintained for quite a long time). Bottom line: Yes, it can be done in React, but with a fair amount of caution.

👍 Enjoy!

last modified on 28th May 2023


If you find anything in this post that should be improved (either factually or in language), feel free to edit it on Github .