2

How can I stop a nested button triggering the parent container Link using stopPropagation? I've checked the other question/answers but I still can't stop it happening. I have the following setup:

{feedData && feedData.map(post => (
      <Link to={`/buy/${post.name}`} key={post.id}>
          <Card />
      </Link>
))}

The Card pulls in an Upvote component with a button inside it:

const Card = ({id}) => {
    return (
        <div>
            ...
            <Upvote id={id}/>
        </div>
    )
}

The Upvote component is basically a button:

<button onClick={(e) => e.stopPropagation() | unlike()}>
    <span>{likes}</span>
</button>

Can anyone see what I'm doing wrong, please?

Ray Purchase
  • 681
  • 1
  • 11
  • 37

1 Answers1

4

You can see in the console.log in the example below how the click event is already not reaching the <a> element due to e.stopPropagation(), but the default behavior would still trigger unless you also use e.preventDefault():

const App = () => {  
  const handleLinkClick = React.useCallback((e) => {    
    console.log('Link Click');
  }, []);
  
  const handleButtonClick = React.useCallback((e) => {
    e.stopPropagation();
    
    // This is what you are missing:
    e.preventDefault();
    
    console.log('Button Click');
  }, []);
  
  return (<React.Fragment>
    <a href="https//www.google.com" target="_blank" onClick={ handleLinkClick }>
      <div>I'm a link!</div>      
      <button onClick={ handleButtonClick }>I'm a button!</button>
    </a>
  </React.Fragment>);
}

ReactDOM.render(<App />, document.querySelector('#app'));
body,
button {
  font-family: monospace;
}

body, p {
  margin: 0;
}

a {
  display: block;
  border: 2px solid black;
  padding: 8px;
  margin: 8px;  
}

a:hover {
  background: yellow;
}

button {
  margin: 16px 0 0;
  padding: 8px;
  border: 2px solid black;
  background: white;
  cursor: pointer;
  border-radius: 2px;
}

button:hover {
  background: cyan;
}

.as-console-wrapper {
    max-height: 67px !important;
}
<script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>

<div id="app"></div>

In any case, that's invalid HTML, so you might want to consider a different solution:

You can overlay an empty <a> element on top of all the other content inside <Card> and make sure the interactive bits like buttons or nested links are drawn on top using z-index. Also, there's no need for e.stopPropagation() or e.preventDefault(), but you might want to add aria-label to describe what it does:

const App = () => {  
  const handleLinkClick = React.useCallback((e) => {    
    console.log('Link Click');
  }, []);
  
  const handleButtonClick = React.useCallback((e) => {
    // No need for these now:
    // e.stopPropagation();
    // e.preventDefault();
    
    console.log('Button Click');
  }, []);
  
  return (<div className="item">
    <div>I'm a link!</div>      
    <button onClick={ handleButtonClick }>I'm a button!</button>
    <a href="https//www.google.com" target="_blank" onClick={ handleLinkClick } aria-label="Open Google"></a>
  </div>);
}

ReactDOM.render(<App />, document.querySelector('#app'));
body,
button {
  font-family: monospace;
}

body, p {
  margin: 0;
}

.item {
  display: block;
  border: 2px solid black;
  padding: 8px;
  margin: 8px;  
  position: relative;
}

.item:hover {
  background: yellow;
}

button {
  margin: 16px 0 0;
  padding: 8px;
  border: 2px solid black;
  background: white;
  cursor: pointer;
  border-radius: 2px;
  position: relative;
  z-index: 1;
}

button:hover {
  background: cyan;
}

a {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
}

.as-console-wrapper {
    max-height: 67px !important;
}
<script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>

<div id="app"></div>
Danziger
  • 19,628
  • 4
  • 53
  • 83
  • 1
    Thank you @Danziger, adding the preventDefault solved the problem. I will eventually use the z-index solution I think, as it will prevent the browser status bar suggesting that clicking on the nested button is going to take them to the Link when it's not. I had thought preventDefault was just for preventing default Form actions for some reason. – Ray Purchase May 16 '21 at 14:19