- 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