2

I have a component where-in I need to fetch some data and render it. The component gets rendered initially. The problem I'm facing is when the handler function switchDocumentType is called after clicking the button for a particular type, the whole component gets unmounted/un-rendered.

While debugging on my own I found this happens after setDocumentType is run inside event handler function. What is wrong in the below code snippet that could possibly cause this issue? I can see the useEffect is not going in infinite-loop as well.

Code snippet:

import * as React from 'react';

const MyComponent = (props) => {
  const [documentType, setDocumentType] = React.useState('alpha');
  const [documentData, setDocumentData] = React.useState('');
  const types = ['alpha', 'beta', 'gamma'];

  React.useEffect(() => {
    myDataFetch('https://example.com/foo/?bar=123').then(async (response) => {
      const data = await response.json();
      setDocumentData(data.terms); // html string
      const myDiv = document.getElementById('spacial-div');
      myDiv.innerHTML = data; // need to render raw HTML inside a div
    });
  }, [documentType]);

  const switchDocumentType = (type) => {
    setDocumentType(type);
    // send some analytics events
  };

  const convertToPDF = () => {
    // uses documentData to generate PDF
  };

  return (
    <div className="container-div">
      {types.map((type) => {
        return (
          <button key={type} onClick={(type) => switchDocumentType(type)}>
            {type}
          </button>
        );
      })}
      <div id="special-div" />
    </div>
  );
};

export default MyComponent;

EternalObserver
  • 517
  • 7
  • 19
  • Components re-render when their state is updated. Effects run when their dependencies are updated. This is expected behavior. Is your response JSON or HTML? I can't tell from the code posted. – Hunter McMillen Dec 03 '22 at 17:10
  • If you dont use `documentData` then comment `setDocumentData(data)` inside the useEffect, because setting the state inside the useEffect will cause the component to re-render, ideally the response should return json data, which can be used to render elements in React – Azzy Dec 03 '22 at 17:13
  • You should also use [`dangerouslySetInnerHTML`](https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml) instead of manipulating the DOM directly. – Hunter McMillen Dec 03 '22 at 17:19
  • @Azzy updated the code snippet to show how I'm using the `documentData` state. Also, the issue is not just the HTML I fetched getting removed, the whole component along with the buttons is getting removed from component tree as per my observation in React-Dev tools(re-rendering shouldn't cause that). – EternalObserver Dec 03 '22 at 17:41
  • @HunterMcMillen sure, will change it to `dangerouslySetInnerHTML` in actual code. – EternalObserver Dec 03 '22 at 17:44
  • do not use dangerouslySetInnerHTML on a react tree... – sheepiiHD Dec 03 '22 at 18:01
  • Will replacing `setDocumentData(data.terms)` with `documentDataRef.current = data.terms`, by using a `useRef` like this `const documentDataRef = useRef('')` solve the problem in you case – Azzy Dec 03 '22 at 18:30
  • @Azzy just an assumption but I don't think using ref would work. While debugging I could find the issue that, inside the handler function, as soon as ` setDocumentType` is called, the whole component is unmounted. The jest of my question is, why updating the internal state of a component is causing it to get unmounted and not getting re-renderred? – EternalObserver Dec 03 '22 at 18:38
  • Generally re rendering a parent causes the child to unmount, but I can't see any callback being called from the parent to change its state, and useEffect runs twice in dev stritct mode, I was wondering does setting `innerHTML` cause it to unmount, – Azzy Dec 03 '22 at 18:46
  • Do you think some parent component also reacting to the click – Azzy Dec 03 '22 at 18:50
  • @Azzy no, the parent component doesn't get affected by the state of child in this case, this was the first thing I checked. – EternalObserver Dec 03 '22 at 18:54
  • Did you try event.preventDefault as mentioned in the answer below. – iaq Dec 04 '22 at 09:12
  • @iaq yes, that ha no affect – EternalObserver Dec 04 '22 at 09:14
  • @EternalObserver the problem seems interesting, is it possible to create a minimal reproducible example, if you are able to figure out the problem, please post the solution, I am curious to know what was the cause – Azzy Dec 04 '22 at 09:32
  • @Azzy sure. Although I'm now suspecting the issue might be related to this (https://stackoverflow.com/questions/56442582/react-hooks-cant-perform-a-react-state-update-on-an-unmounted-component). Returning a cleanup-function from useEffect has solved my issue. – EternalObserver Dec 04 '22 at 11:28

3 Answers3

0

Do not use useEffect as handler, use useEffect hooks for initializations. Instead of using/setting innerHtml, let react do it for you. I suppose you have myDataFetch defined somewhere and I don't see your data fetch using the type.

Anyways, try to use the modified code below.

   import * as React from 'react';

const MyComponent = (props) => {
  const [documentType, setDocumentType] = React.useState('alpha');
  const [documentData, setDocumentData] = React.useState('');
  const types = ['alpha', 'beta', 'gamma'];

  const fetchData = async () => {
    const response = await myDataFetch('https://example.com/foo/?bar=123')
    const data = await response.json();
    setDocumentData(data);
  }

  React.useEffect(() => {
    fetchData();
  }, []);

  const switchDocumentType = async (e, type) => {
    e.preventDefault();
    setDocumentType(type);
    await fetchData();
    // send some analytics events
  };

  return (
    <div className="container-div">
      {types.map((type) => {
        return (
          <button key={type} onClick={(e) => switchDocumentType(e, type)}>
            {type}
          </button>
        );
      })}
      <div id="special-div">{documentData}</div>
    </div>
  );
};

export default MyComponent;
iaq
  • 173
  • 1
  • 2
  • 10
  • Even after refactoring the code as you mentioned, the issue still persists. – EternalObserver Dec 03 '22 at 18:09
  • Can you check in the browser dev tools network tab what do you get in response data after clicking the button? Also you don't need a parameter type for your onclick handler. I have modified the code. Try again. – iaq Dec 03 '22 at 18:18
  • yes, I'm getting the response. – EternalObserver Dec 03 '22 at 18:21
  • Try now, I have updated the code. Added preventDefault using the event and removed the type as parameter – iaq Dec 03 '22 at 18:28
0

You shouldn't edit the DOM directly. React has two DOMs, a virtual DOM and a real DOM. Rendering can be a bit finicky if you decide to edit the real DOM.

You can parse html safely, by using html-react-parser. This is the best way to do it, because it becomes part of the react tree whereas dangerouslySetInnerHTML will replace the entire HTML to flush changes to the DOM. With reconciliation, it can create exponential load times.

It will also sanitize your inputs, you know.. for safety. :)

import parse from 'html-react-parser';

const SpecialDiv = ({html}) => {
   const reactElement = parse(html);
   return reactElement
}

If you decide that you must use dangerouslySetInnerHTML you can do it as so:

const [someHTML, setSomeHTML] = useState(null)

const someFunction = async() => {
   const response = await getData();
   const data = await response.json();

   setSomeHTML(data);
}

return( 
   <div>
      {someHTML && <div dangerouslySetInnerHTML={{__html: someHTML}} id="special-div"/>}
   </div>
)

That being said, I would say that by allowing this, you open yourself up to the possibility of a XSS attack, without properly parsing and purifying your inputs.

sheepiiHD
  • 421
  • 2
  • 16
  • I know all of this but I guess it is unrelated to my issue. Also, I don't have the option to add yet another dependency in my code just to append a small HTML content. – EternalObserver Dec 03 '22 at 18:12
  • You can use `setDangerouslySetInnerHTML`, it's just not recommended for the reasons above. – sheepiiHD Dec 03 '22 at 18:14
0

Not sure why but placing debuggers before state update causes this issue, not only for this component, but for all the other components I tried with. Seems to be an issue either with debugger or React. Removing debuggers solved the issue.

Also, now I'm returning a cleanup function inside useEffect as pointed out in some stack-overflow posts. I also refactored the code as suggested by @iaq and @sheepiiHD to follow React best practices.

Updated code:

import * as React from 'react';

const MyComponent = (props) => {
  const [documentType, setDocumentType] = React.useState('alpha');
  const [documentData, setDocumentData] = React.useState('');
  const types = ['alpha', 'beta', 'gamma'];

  const fetchData = async () => {
    const response = await myDataFetch('https://example.com/foo/?bar=123')
    const data = await response.json();
    setDocumentData(data);
  }

  React.useEffect(() => {
    fetchData();
    return () => {
      setDocumentType('');
      setDocumentData('');
    };
  }, []);

  const switchDocumentType = async (e, type) => {
    e.preventDefault();
    setDocumentType(type);
    await fetchData();
    // send some analytics events
  };

  return (
    <div className="container-div">
      {types.map((type) => {
        return (
          <button key={type} onClick={(e) => switchDocumentType(e, type)}>
            {type}
          </button>
        );
      })}
      <div id="special-div" dangerouslySetInnerHTML={{__html: documentData.terms}} />
    </div>
  );
};

export default MyComponent;
EternalObserver
  • 517
  • 7
  • 19
  • I guess something funny in your documentData.terms is causing this issue. If the cleanup function is called, this means you are unmounted. Check you might have an element with id root in your documentData.terms or some other conflicting element tags. Try using the value directly instead of using it as html like
    {document.terms}
    – iaq Dec 04 '22 at 23:37