Preact: Synchronous Event Bubbling/Mounting Difference vs React

Created on 25 Aug 2017  路  10Comments  路  Source: preactjs/preact

(Moved from https://github.com/developit/preact-compat/issues/418, apologies if this is the wrong place)

Hey there!

First, thank you for the awesome library 馃憤 , I've had a surprisingly painless process converting multiple React apps to Preact. I did, however, run into an issue the other day regarding what I believe to be a difference between event handling and the component lifecycle in React and Preact. Basically:

  • In React, components rendered due to an event (i.e. a Modal opening in response to a click event) are mounted _after_ that event bubbles to the document root.
  • In Preact (with or without preact-compat), the newly-mounted component mounts _before_ that event bubbles, and thus is able to capture it, which can lead to unexpected issues. I noticed this after registering document event listeners on a Modal's componentDidMount, and seeing that it started capturing the click event that caused the modal to open in the first place.

I have provided a demo below for both the Preact and React use case. If you check the console output you should be able to see the difference I'm describing:

// console logs on button click:
1) Example mounted
2) Document noticed a click event
3) Example noticed a document click event
// console logs on button click:
1) Document noticed a click event
2) Example mounted

For Reference

document.addEventListener('click', () => {
  console.log('Document noticed a click event')
})

export class Example extends Component {
  componentDidMount () {
    console.log('Example mounted!')
    // we can fix this by wrapping the addEventListener call in a `setTimeout` to
    // let the current call stack complete.
    document.addEventListener('click', (e) => {
      console.log('Example noticed a document click event')
      this.props.onClose()
    })
  }

  render () {
    return <h1>Example Thing</h1>
  }
}

export default class App extends Component {
  state = { show: false }

  render () {
    return (
      <div>
        <button onClick={() => this.setState({ show: true })}>Show Thing</button>
        {this.state.show && <Example onClose={() => this.setState({ show: false })} />}
      </div>
    )
  }
}

Thanks in advance!

discussion help wanted

All 10 comments

Hi there! This is definitely a difference between the two libraries. Unfortunately, I'm not sure it's one we can really fix - Preact is too thin of a wrapper over DOM events to change the timing in such a way.

In terms of mitigation techniques, using .stopPropagation() in the click handler works in both libraries and produces the same result in each. It might be considered a reasonable "safeguard":

https://jsfiddle.net/developit/v6uot3Lv/

@developit thank you for the response. I have implemented a workaround, and as you said stopping propagation definitely is a viable alternative (although I think the timeout approach more accurately reflects what happens in React's lifecycle).

If you do not have plans to resolve this difference you are more than welcome to close this, I just wanted to bring it to your attention in case it was not a known difference. Otherwise, I will keep an eye on it and attempt to contribute where possible. I'm not yet close enough to Preact's (or, for that matter, React's) internals to know whether it's a resolvable difference.

I'm happy to leave it open - it's something that I'd love to address in theory, just not sure how to do it without adding synthetic events to Preact (which would be a non-starter). Maybe someone will drop some wisdom in here for us to glean ;)

I think this could be solved with implicit event delegation, which would also make adding events a constant time O(1) operation and the other benefits that come with event delegation.

@thysultan how would you avoid having to reimplement bubbling? Delegating from a root is fine but I don't see how it avoids reimplementing events or manually constructing shadow events to mimic propagation.

@developit Yes i suppose that is a caveat with event delegation. I'm out of ideas that maintain sync DOM operations.

For reference, I think this question on stackoverflow can be related to this issue. A simple button callback gets fired twice:

https://stackoverflow.com/questions/52950913/preactjs-onclick-toggler-firing-twice?noredirect=1#comment92820864_52950913

Fiddle here

@abidibo Looks we fixed it in Preact X. I'm unable to reproduce the issue anyomre :tada:

Regarding the original issue: I'm unsure how we should proceed here. Preact reuses the DOM's event system and doesn't ship with its own variant like react does.

Always fun to revisit issues from almost 2 years back. I'm actually using Preact X in a new project and thus far haven't encountered the issue.

Yeah I was going through some old ones to do some house keeping. That's awesome. If you don't mind I'll close this issue since this doesn't seem to be as much of an issue anymore with Preact X like you said馃憤馃帀

Was this page helpful?
0 / 5 - 0 ratings