Ray Purchase
Ray Purchase

Reputation: 762

How can I stop a child button triggering a parent Link in React?

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?

Upvotes: 2

Views: 1740

Answers (1)

Danziger
Danziger

Reputation: 21161

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/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/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/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

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

Upvotes: 4

Related Questions