Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@naholyr
Last active November 15, 2019 13:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save naholyr/a7263e4b02d166bfa0134131c0f7c4c2 to your computer and use it in GitHub Desktop.
Save naholyr/a7263e4b02d166bfa0134131c0f7c4c2 to your computer and use it in GitHub Desktop.
import * as React from 'react';
// TODO inject counters
const Counter = () => {
const [value, setValue] = React.useState(1);
const incr = React.useCallback(() => setValue(value + 1), [value]);
return (
<button type="button" onClick={incr}>
Click to incr. ({value})
</button>
);
};
const Page = ({ html }) => {
return <div dangerouslySetInnerHTML={{ __html: html }} />;
};
const html = `
<!-- dynamic HTML received from API -->
<p>with paragraphs including [COUNTER]</p>
<div>
<p>or nested HTML</p>
<div>
<p>with another [COUNTER] embedded</p>
</div>
</div>
`;
export default () => <Page html={html} />;
/*
Solution 1:
transform
<p>… [MARKER] …</p>
into
<p>… <span data-target-index="0"></span> …</p>
<span data-element-index="0">(the React element here)</span>
then move DOM nodes
*/
const extractCounters = html => {
const counterElements = [];
const updatedHtml = html.replace(/\[COUNTER(?:=(\d+))?\]/g, (match, value) => {
const index = counterElements.length;
const element = (
<span data-element-index={index} key={index}>
<Counter initialValue={Number(value) || 0} />
</span>
);
counterElements.push(element);
return `<span data-target-index="${index}"></span>`;
});
return [updatedHtml, counterElements];
};
const Page = ({ html }) => {
const [updatedHtml, elements] = React.useMemo(() => extractCounters(html), [html]);
const ref = React.useRef();
React.useEffect(() => {
// Move elements into dynamic HTML
elements.forEach((element, index) => {
ref.current
.querySelector(`[data-target-index="${index}"]`)
.appendChild(ref.current.querySelector(`[data-element-index="${index}"]`));
});
// TODO Should move elements back to queue in cleanup callback?
});
return (
<div ref={ref}>
<div dangerouslySetInnerHTML={{ __html: updatedHtml }} />
{elements}
</div>
);
};
/*
Solution 2:
transform
<p>… [MARKER] …</p>
into
<p>… <span data-target-index="0"></span> …</p>
then use ReactDOM.render to dynamize spans as mount points
*/
const extractCounters = html => {
const counterElements = [];
const updatedHtml = html.replace(/\[COUNTER(?:=(\d+))?\]/g, (match, value) => {
const index = counterElements.length;
const element = <Counter initialValue={Number(value) || 0} />;
counterElements.push(element);
return `<span data-target-index="${index}"></span>`;
});
return [updatedHtml, counterElements];
};
const Page = ({ html }) => {
const [updatedHtml, elements] = React.useMemo(() => extractCounters(html), [html]);
const ref = React.useRef();
React.useEffect(() => {
// Move elements into dynamic HTML
elements.forEach((element, index) => {
ReactDOM.render(element, ref.current.querySelector(`[data-target-index="${index}"]`));
});
// TODO How to clean up properly (if required)?
});
return <div ref={ref} dangerouslySetInnerHTML={{ __html: updatedHtml }} />;
};
  • I feel like solution 2 highly disturbing by calling ReactDOM.render as a component's side-effect
  • However, solution 1 feels more clunky and pollutes DOM more
  • I could not thing of any other way yet…

Live demo: codesandbox.io/s/jw5pq

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