Sanitized HTML can be turned into React Components that can be run both on server and client by parsing the html string and transforming the resulting nodes into React elements.
const React = require('react');
const ReactDOMServer = require('react-dom/server');
const str = `<div>divContent<p> para 1</p><p> para 2</p><gallery image-ids="" /><player video-id="" /><p> para 3</p><gallery image-ids="[1, 3]"/></div>`;
var parse = require('xml-parser');
const Gallery = () => React.createElement('div', null, 'Gallery comp');
const Player = () => React.createElement('div', null, 'Player comp');
const componentMap = {
gallery: Gallery,
player: Player
};
const traverse = (cur, props) => {
return React.createElement(
componentMap[cur.name] || cur.name,
props,
cur.children.length === 0 ? cur.content: Array.prototype.map.call(cur.children, (c, i) => traverse(c, { key: i }))
);
};
const domTree = parse(str).root;
const App = traverse(
domTree
);
console.log(
ReactDOMServer.renderToString(
App
)
);
Note however, it is not JSX/TSX that you really need, as you mentioned, but a tree of React Nodes for the React renderer (ReactDOM in this case). JSX is just syntactic sugar, and transforming it back and forth is unnecessary unless you want to maintain the React output in your codebase.
Pardon the over simplified html parsing. Its only for illustrative purposes. You might want to use a more spec-compliant library to parse the input html or something that fits your use case.
Make sure, the client side bundle get the exact same App
component, or else you might React's client side script would re-create the DOM tree and you'll lose all the benefits of server side rendering.
You can take advantage of the React 16's streaming out too with the above approach.
Addressing the props problem
Props will be available to you from the tree as attributes and can be passed as props (on careful consideration of your use case ofcourse).
const React = require('react');
const ReactDOMServer = require('react-dom/server');
const str = `<div>divContent<p> para 1</p><p> para 2</p><gallery image-ids="" /><player video-id="" /><p> para 3</p><gallery image-ids="[1, 3]"/></div>`;
var parse = require('xml-parser');
const Gallery = props => React.createElement('div', null, `Gallery comp: Props ${JSON.stringify(props)}`);
const Player = () => React.createElement('div', null, 'Player comp');
const componentMap = {
gallery: Gallery,
player: Player
};
const attrsToProps = attributes => {
return Object.keys(attributes).reduce((acc, k) => {
let val;
try {
val = JSON.parse(attributes[k])
} catch(e) {
val = null;
}
return Object.assign(
{},
acc,
{ [ k.replace(/-/g, '') ]: val }
);
}, {});
};
const traverse = (cur, props) => {
const propsFromAttrs = attrsToProps(cur.attributes);
const childrenNodes = Array.prototype.map.call(cur.children, (c, i) => {
return traverse(
c,
Object.assign(
{},
{
key: i
}
)
);
});
return React.createElement(
componentMap[cur.name] || cur.name,
Object.assign(
{},
props,
propsFromAttrs
),
cur.children.length === 0 ? cur.content: childrenNodes
);
};
const domTree = parse(str).root;
const App = traverse(
domTree
);
console.log(
ReactDOMServer.renderToString(
App
)
);
Careful with custom attributes though - you might want to follow this rfc. Stick with camelCase if possible.