- 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
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 }} />; | |
}; |
Live demo: codesandbox.io/s/jw5pq