Search code examples
javascriptreactjsreact-hooksreact-typescript

Override anchor tag clicks and update in dynamically rendered html in react


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>
    )
}

Solution

  • 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 div
    • querySelectorAll to get the a tags
    • addEventListener to add the listeners
    useEffect(() => {
      // 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}
        </>
      );
    }