I’m writing a design tool that first parses a piece of html and then re-renders it with a tree of React components like this:
const ArtboardNode = function ({ node }) {
const elementProps = node.attributes.toJSON();
const children = node.hasChildren
? node.children.map((n) => <ArtboardNode key={n.id} node={n} />)
: undefined;
return React.createElement(node.tagName, elementProps, children);
}
Here, node
is a custom object already (corresponding to an HTMLElement
) that I’m using as an intermediate representation for the document.
This is already working somewhat, but there are some issues:
- React gives runtime warnings on attribute names that do not match the respective property/React names (
class
vsclassName
, etc.). - React gives a runtime error when a
style
attribute is present, as it doesn’t like a string for that one. There may be other attributes with such a problem.
Obviously I could now start to hack around these issues, such as manually building a name mapping and add special handing for some special cases such as style
.
I’m asking if somebody either knows how to deal with points 1+2 better or has a better approach in general. I also considered not using React for this part at all, but I’d like to have some intermediate state for the editor to work on and the way it’s already set up it would be very easy to implement things like undo operations.
4
There are essentially two choices, which are very different:
Option One – React “raw DOM” escape hatch
This is somewhat similar to “not using React” in the sense that it makes the injected DOM not directly subject to React’s VDOM algo but keeps the injection logic colocated with your React component.
Using dangerouslySetInnerHTML
, you can have React inject DOM nodes constructed from a string. Those DOM nodes are then driven by that string.
This isn’t the whole solution for you since you have only an object representation of the HTML element at hand, not a whole HTML string ready to inject. You could construct such a thing just to use with dangerouslySetInnerHTML
, but that seems obtuse.
However, DOM nodes created with dangerouslySetInnerHTML
have the unique property of not being touched by React’s re-render process unless the string in the __html
property of the object passed to dangerouslySetInnerHTML
changes, or the element that dangerouslySetInnerHTML
is declared on unmounts. This means the DOM elements created by it, unlike the norm in React, can be manipulated by imperative direct DOM calls, and you don’t fundamentally violate React’s internal model in the process. It is, however, esoteric to do something like his.
Practically, you use dangerouslySetInnerHTML
just to create an empty container. You then use the raw DOM API to append to it.
import React, { useId, useLayoutEffect } from "react";
const ArtboardNode = function ({ node }) {
const id = useId();
useLayoutEffect(() => {
const createDomEl = (currentNode, parentEl) => {
const el = document.createElement(currentNode.tagName);
Object.entries(currentNode.attributes).forEach(([name, val]) =>
el.setAttribute(name, val),
);
parentEl.appendChild(el);
if (currentNode.hasChildren) {
currentNode.children.forEach((child) => createDomEl(child, el));
}
};
createDomEl(node, document.getElementById(id));
}, [node, id]);
return <div dangerouslySetInnerHTML={{ __html: `<div id="${id}"></div>` }} />;
};
I have not tested this, but as pseudo code it would achieve converting the node
structure to DOM nodes without interfacing first with React’s internal element constructs.
Option Two – Parsing
As you imply, it’s heavy lifting. but libraries exist to parse HTML (which could be created or derived as a string from your intermediary object) into React constructs.
How useful this really is depends on use case.
8