score:1

Accepted answer

I faced quite the similar issue on one of my apps and I think that the solution is to avoid using the react-router <Link /> component if you can.

Instead of using the <Link /> component, I used the withRouter() HOC provided by react-router :

What you wrote :

Class MyComponent extends React.Component{
  <Link onClick={(e) => { e.stopPropagation(); }} [...]>
    ...
  </Link>
}

export default MyComponent;

Can be replaced by :

Class MyComponent extends React.Component{

  navigate = (event, location) => {
    event.preventDefault();
    this.props.history.push(location);
  }

  <a onClick={(e, href) => { this.navigate(e, href) }} [...]>
    ...
  </a>
}

export default withRouter(MyComponent);

This should prevent your link to catch others event at the document level and achieve the same result as <Link /> component.

score:2

Update: It seems that there's no simple way to do this without rendering the whole app inside an iframe.

If you don't mind doing that though, it's easy enough (taken from How to set iframe content of a react component):

const Frame = ({ children }) => {
    const [ref, setRef] = useState(null)
    const mountNode = ref?.contentWindow.document.body

    return (
        <iframe title="iframe" ref={setRef}>
            {mountNode && ReactDOM.createPortal(children, mountNode)}
        </iframe>
    )
}

Then, wrap your main app as a child of the Frame component. Clicks inside an iframe will not bubble up to the parent window, as detailed in Capture click on div surrounding an iframe.

Here's a CodeSandbox demo showing this approach.


Original (non-functioning) answer below

On your App or Router component:

const blockClicks = useCallback((e) => {
    const { target } = e
    const { nodeName, href } = target
    if (
        target.closest('#root-id') &&
        nodeName === 'A' &&
        href &&
        (/^https?:\/\//.test(!href) ||
        href.startsWith(window.location.origin)
    )) {
        // amend logic specific to your use case
        // - we want to avoid blocking irrelevant clicks
        e.stopPropagation()
    }
})

useEffect(() => {
    document.addEventListener('click', blockClicks, true)

    return () =>
        document.removeEventListener('click', blockClicks, true)
})

Related Query

More Query from same tag