question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Removing the container element after CLOSE/DESTROY zoid event (after close()), results in uncaught error

See original GitHub issue

Hi, I’ve running into this trouble and I couldn’t sort it out.

Scenario

  • Parent site using React v17.
  • User opens an HTML modal, which instantiates and renders a Zoid component (using React driver), as an iframe.
  • User Clicks on the background to close it, which internally executes zoidComponent.close() to clean the zoid frame.
  • If enough time has passed, so the iframe was already loaded and rendered properly, this results in no errors ✅.
  • But if the user opens the modal and quickly closes it, (seemingly, before the zoid iframe finished loading), then the following uncaught error is thrown 🛑.
  • This is happening because the container component from the view, is being set for removal right after the EVENTS.CLOSED zoid event.
  • What I’d expect: no error thrown when removing the modal container from the view, after the EVENTS.CLOSED (or, EVENTS.DESTROYED) event - or a way to await for the component to properly be disposed, or a way to handle/catch the error.
zoid.frameworks.frame.js:3201 Uncaught Error: Detected container element removed from DOM
    at zoid.frameworks.frame.js:3201
    at anonymous::once (zoid.frameworks.frame.js:1045)
    at elementClosed (zoid.frameworks.frame.js:3165)
    at removeChild (react-dom.development.js:10301)
    at unmountHostComponents (react-dom.development.js:21296)
    at commitDeletion (react-dom.development.js:21347)
    at commitMutationEffects (react-dom.development.js:23407)
    at HTMLUnknownElement.callCallback (react-dom.development.js:3945)
    at Object.invokeGuardedCallbackDev (react-dom.development.js:3994)
    at invokeGuardedCallback (react-dom.development.js:4056)
    at commitRootImpl (react-dom.development.js:23121)
    at unstable_runWithPriority (scheduler.development.js:646)
    at runWithPriority$1 (react-dom.development.js:11276)
    at commitRoot (react-dom.development.js:22990)
    at performSyncWorkOnRoot (react-dom.development.js:22329)
    at react-dom.development.js:11327
    at unstable_runWithPriority (scheduler.development.js:646)
    at runWithPriority$1 (react-dom.development.js:11276)
    at flushSyncCallbackQueueImpl (react-dom.development.js:11322)
    at flushSyncCallbackQueue (react-dom.development.js:11309)
    at scheduleUpdateOnFiber (react-dom.development.js:21893)
    at dispatchAction (react-dom.development.js:16139)
    at Object.onDestroy (App.tsx:61)
    at zoid.frameworks.frame.js:3320
    at zoid.frameworks.frame.js:2746
    at Function.ZalgoPromise.try (zoid.frameworks.frame.js:387)
    at _loop (zoid.frameworks.frame.js:2745)
    at Object.trigger (zoid.frameworks.frame.js:2749)
    at zoid.frameworks.frame.js:2967
    at Function.ZalgoPromise.try (zoid.frameworks.frame.js:387)
    at destroy (zoid.frameworks.frame.js:2966)
    at zoid.frameworks.frame.js:2983
    at ZalgoPromise._proto.dispatch (zoid.frameworks.frame.js:239)
    at ZalgoPromise._proto.then (zoid.frameworks.frame.js:275)
    at zoid.frameworks.frame.js:2982
    at Function.ZalgoPromise.try (zoid.frameworks.frame.js:387)
    at zoid.frameworks.frame.js:2975
    at anonymous::memoized (zoid.frameworks.frame.js:998)
    at HTMLDivElement.overlay.onclick (pluggy-connect.ts:199)

Context

Below is how the user (parent site) is triggering the “close” action of the iframe which is rendered on the modal. Then, it listens for the zoid CLOSE / DESTROY events, and after that it executes a callback so the container modal can be effectively removed from the view.

      containerTemplate({
        doc,
        dimensions: { height, width },
        close,
        uid,
        frame,
        prerenderFrame,
        event,
        props,
      }) {
// ...
        // zoid component 'close' event handler
        event.on(EVENT.CLOSE, () => {
          document.body.classList.remove(modalVisibleClassName);
          const { onClose } = props;
          // callback for the parent site so it can update the view
          if (onClose) {
            onClose();
          }
        });

        // zoid component 'destroy' event handler
        event.on(EVENT.DESTROY, () => {
          const { onDestroy } = props;
          // callback for the parent site so it can update the view
          if (onDestroy) {
            onDestroy();
          }
        });

          // overlay modal close handler 
          overlay.onclick = () => {
            close()
          };
// ...

This is how the parent app is instantiating the component:

export const MyReactZoidComponent = ({
  tag,
  url,
  ...props
}) => {
  const Zoid = useMemo(() => {
    zoid.destroy()
    return zoid.create({ tag, url }).driver("react", { React, ReactDOM })
  }, [tag, url])

  return Zoid ? <Zoid {...props} /> : null
}

And this is how the parent site renders it:

function App() {
  const [isModalOpened, setIsModalOpened] = useState(false);

  const myZoidComponentProps: MyZoidComponentProps = {
    url: REACT_APP_CONNECT_URL,
    connectToken: REACT_APP_API_KEY,
    onError: (error) => {
      console.error('Whoops! Error result... ', error);
    },
    onSuccess: (data) => {
      console.log('Yay! Success!', data);
    },
    onOpen: () => {
      console.log('Modal opened.');
    },
    onClose: () => {
      console.log('Pluggy Connect modal closed.');
      // setIsModalOpened(false);
    },
    onDestroy: () => {
      console.log('Pluggy Connect modal destroyed.');
      try {
        setIsModalOpened(false);
      } catch (error) {
        // NOTE: this catch isn't working, even though the line above shows up in the stack-trace
        console.error('whoops..', error);
      }
    },
  };

  const openModal = () => {
    setIsModalOpened(true);
  };

  return (
    <div className="App">
      <button onClick={openModal}>Open Modal</button>
      {isModalOpened && <MyReactZoidComponent {...myZoidComponentProps} />}
    </div>
  );
}

What I’ve tried

  1. Adding a try {} catch {} block arround the close() method, and also a promise .catch(). None have worked.
          overlay.onclick = () => {
            try {
              close().catch((error: unknown) =>
                console.warn('Modal close handler resulted in an error ', error)
              );
            } catch(error) {
              console.warn('Caught! Modal close handler resulted in an error ', error)
            }
          };
  1. Surrounding the <Zoid/> react component with an error boundary. Didn’t work.

**What I’d expect **

  • The zoid component close() call should not result in an unhandled error, or
  • The error should be able to be caught/handled using .catch() or try {} catch {} in some place, or…
  • A working way to properly await for the .close() / EVENTS.CLOSE action to be fully completed to let me safely remove the container component from the page.

Issue Analytics

  • State:open
  • Created 3 years ago
  • Comments:11 (5 by maintainers)

github_iconTop GitHub Comments

1reaction
gregjopacommented, Jan 8, 2021

Hi @tmilar, I was reviewing your implementation and was wondering if you could solely rely on the zoid close() function for cleaning up the DOM instead of having the React component do it. Ex:

export const MyReactZoidComponent = ({
  tag,
  url,
  ...props
}) => {
  const Zoid = useMemo(() => {
    zoid.destroy()
    return zoid.create({ tag, url }).driver("react", { React, ReactDOM })
  }, [tag, url])

  // return Zoid ? <Zoid {...props} /> : null
  // always return the zoid component here no matter what. Let the close() function be responsible for DOM cleanup.
  return <Zoid {...props} />
}

The zoid close() function should be responsible for removing the DOM of the zoid component. There are some MutationObservers being used with zoid and and calling close() cleans everything up properly. If you destroy it yourself by having React rip the component out of the DOM then you will end up seeing the error “Uncaught Error: Detected container element removed from DOM”.

One other thing to note is the close() function is async. You’ll want ensure that is done before having React remove it (ex: return Zoid ? <Zoid {...props} /> : null).

          // overlay modal close handler 
          overlay.onclick = () => {
            close().then(() => {
              console.log('closing successful!');
            });
          };
0reactions
badeAdebayocommented, Nov 24, 2021

any update on this issue? @bluepnume i see this error every time i click the back button before the iframe finishes loading

Read more comments on GitHub >

github_iconTop Results From Across the Web

krakenjs - Bountysource
Angular app - Stacktrace: Error: Request listener already exists for ... Removing the container element after CLOSE/DESTROY zoid event (after close()), ...
Read more >
zoid - bytemeta
zoid repo issues. ... Removing the container element after CLOSE/DESTROY zoid event (after close()), results in uncaught error. MortyPAA. MortyPAA CLOSED.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found