I am making an api call in react to fetch html and bind to the inner html of a div in react. The issue I am currently facing is that dynamically rendered html has links that I want to override on click and update the state in react.
How do I go about it?
Below is code on the work I have done so far.
export const HtmlReportViewer:React.FC<HtmlReportViewerProps> = (props) => {
const {reportPath, reportParams} = props;
const htmlStringRef = React.useRef();
const [htmlString, setHtmlString] = React.useState('<p>Hello</p>')
const getReportAsHtml = async () => {
const payload = {
ReportPath: reportPath,
JsonParameters: JSON.stringify(paramsToArray(reportParams)),
RenderAs: "HTML4.0"
}
const rawResponse = await fetch('api/ReportServer/GetHtml', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Options': JSON.stringify(payload),
},
});
return await rawResponse.text();
}
React.useEffect(() => {
// make call to set report html string to report
getReportAsHtml().then((data) => {
setHtmlString(data);
})
})
return (
<div ref={htmlStringRef} dangerouslySetInnerHTML={{__html: htmlString}}>
</div>
)
}
I've made a working sandbox
First of all, your useEffect
should have a dependency array, because now it will run every render
useEffect(() => {
getReportAsHtml().then((data) => {
setHtmlString(data);
});
}, []);
You can't use react's API to capture events, because you insert HTML directly, so you have to add listeners yourself.
I would use
MutationObserver
to detect changes to your divquerySelectorAll
to get the a
tagsaddEventListener
to add the listenersuseEffect(() => {
// your onclick function
const handler = (e) => {
e.preventDefault();
// do whatever you want
};
// your observer
const observer = new MutationObserver(() => {
// this will run every time the HTML in the div changes
// get the elements
const elements = htmlStringRef.current.querySelectorAll("a");
// assign click listener
elements.forEach((anchor) => anchor.addEventListener("click", handler));
});
// observe
observer.observe(htmlStringRef.current, { childList: true });
}, []);
It should work just like this, but not removing the listeners will cause bugs
useEffect(() => {
// array for elements
let elements = [];
const handler = (e) => {
e.preventDefault();
// do whatever you want
};
// function for clearing the listeners
const clearListeners = () =>
elements.forEach((el) => el.removeEventListener("click", handler));
const observer = new MutationObserver(() => {
// before getting new elements, clear old listeners
clearListeners();
elements = htmlStringRef.current.querySelectorAll("a");
elements.forEach((anchor) => anchor.addEventListener("click", handler));
});
observer.observe(htmlStringRef.current, { childList: true });
// remember to remove everything when component is unmounted
return () => {
clearListeners();
observer.disconnect();
};
}, []);
Full code
import { useEffect, useReducer, useRef, useState } from "react";
export default function App() {
const htmlStringRef = useRef(null);
const [htmlString, setHtmlString] = useState();
const [clicks, addClicks] = useReducer((state) => state + 1, 0);
const getReportAsHtml = async () => {
return '<p>some text with a <a href="#">link</a></p>';
};
useEffect(() => {
let elements = [];
const handler = (e) => {
e.preventDefault();
addClicks();
};
const clearListeners = () =>
elements.forEach((el) => el.removeEventListener("click", handler));
const observer = new MutationObserver(() => {
clearListeners();
elements = htmlStringRef.current.querySelectorAll("a");
elements.forEach((anchor) => anchor.addEventListener("click", handler));
});
observer.observe(htmlStringRef.current, { childList: true });
return () => {
clearListeners();
observer.disconnect();
};
}, []);
useEffect(() => {
getReportAsHtml().then((data) => {
setHtmlString(data);
});
}, []);
return (
<>
<div
ref={htmlStringRef}
dangerouslySetInnerHTML={{ __html: htmlString }}
></div>
{clicks}
</>
);
}