Skip to content

[Discussion] dealing with unintentional batched touch events #9288

Closed

Description

@DanielZlotin

There is a problem handling quick succession of touches / clicks. This can be categorised as a performance issue.

Use case:

  • user clicks multiple times and quickly on a button
  • events handled in JS by some click handler function which performs a navigation event
  • by the time the navigation event gets completed (some screen is pushed, for example, which hides the button) 2 more clicks was already on their way and this same screen gets pushed multiple times

This is not a react-native specific problem but it gets compounded by the fact all touch events originate in the native's main thread, dispatched through the bridge asynchronously along with some serialisation mechanism and handled in the JS context (in a different thread).

This is very much like the problem with Android's startActivity(Intent) which is async, and, if the pushed screen takes too much time to appear, can cause this "batching" of events.

I would appreciate your thoughts on this matter and how do you think it can be handled (with minimum hacks in mind).

Some example solutions:

  • When receiving the event immediately ignore all other events (save something in the state) and enable it back once finished - this is hacky, difficult to maintain, causes the codebase to be a ball of mud, and not alway be possible.
  • add a new prop to all buttons (TouchableHighlight, TouchableOpacity etc) that will be called throttle. When you specify throttle=1000 the button will do something like
_.throttle(onClick, 1000, {leading:true, trailing:false})

which just sends the first event and ignores all other touch events on this specific button in a 1000 millisecond window. See lodash throttle
This is also hacky but much simpler to maintain.

Using react-native 0.25.1.
Happens on all platforms.

Activity

  1. talkol commented on Aug 8, 2016

    @talkol

    I want to add a live example showing this issue and why it's relevant particularly under RN:

    https://rnplay.org/apps/cKSvuA

    I recommend running the example on your own physical iOS device (using "React Native Playground" from the App Store)

    Use case:
    Show an iOS alert when a button (TouchableOpacity) is pressed
    Click the button quickly and you'll see more than one Alert
    The example here simulates a stress condition in the app (JS thread being busy)

    Why this isn't usually an issue in pure native implementations
    In pure native code, one would show the alert from the UI thread during the click event handler. Since the alert is displayed synchronously on the UI thread, the user will not be able to tap twice (the UI thread will be blocked until the alert is shown, and when the alert is shown, the button is no longer accessible).

    Why is the problem more prominent in RN?
    Since the entire mechanism is now asynchronous, the touch event needs to be sent over the bridge to JS and then after business logic runs in JS, the resulting change in UI needs to be sent back over the bridge. There are two main potential bottlenecks here:

    • the bridge itself - if the bridge is busy with other data (like other things being sent to native at the same time), there's a delay until it clears up
    • the JS event loop - if JS is busy with lots of other stuff, there's a delay as well

    These two bottlenecks really do happen in complex apps, making the asynchronicity of the entire interaction feel worst

    Where do we suffer from this issue in production?
    Usually when we have one screen in the app, and the user presses a button that pushes a new screen (using some navigation library). Since the new screen pushed usually incurs a significant render, this makes the interaction sluggish on complex screens. We've had jittery users pressing multiple times in succession which results in multiple pushes.

    The point of this discussion
    Obviously every developer can deal with this stuff by himself using some localized band aid. Since the core of the issue is onPress events being sent multiple times when pressed very quickly, it makes sense to add some optional platform-wide mechanism to help deal with this issue. We might even want to add this optional mechanism to all core Touchable components.

  2. adbl commented on Oct 19, 2016

    @adbl
    Contributor

    how come no one shows interest in this issue?

    I think it is a good idea to have throttle/debounce prop on all TouchableX. Since a touch can mean a different things, maybe a better way is to have like onPressThrottle. I think it should have a default setting of something greater than 0 to prevent most of these errors.

  3. DanielZlotin commented on Oct 19, 2016

    @DanielZlotin
    ContributorAuthor

    That's a possible solution, but there are really 2 different "creatures" in this case. The buttons inside navigation controllers - meaning, topBar buttons, FAB, menu icon etc. and the second kind is the regular JSX TouchableX views.

    I'm not against adding a prop of some kind to the navigation buttons, but adding a prop to TouchableX is obviously not something that can be done. A possible solution we can have is to wrap the JSX touchables with a custom view, but then I'm not sure this should be a part of this library.

  4. adbl commented on Oct 19, 2016

    @adbl
    Contributor

    Are the buttons inside navigation controllers etc not using TouchableX components?

    I started looking at the Touchable mixin, https://github.com/facebook/react-native/blob/master/Libraries/Components/Touchable/Touchable.js
    and it does look at certain props of the component which has it mixed in, so I don't see why it's something that cannot be done.

  5. DanielZlotin commented on Oct 19, 2016

    @DanielZlotin
    ContributorAuthor

    Sorry, scratch that, was talking about react-native-navigation 😄 (got mixed up with a similar discussion)

  6. adbl commented on Oct 19, 2016

    @adbl
    Contributor

    Now I'm super confused 😅

    I'm here because users can mash a plain react native TouchableX multiple times and it crashes our app =)

  7. DanielZlotin commented on Oct 19, 2016

    @DanielZlotin
    ContributorAuthor

    yeah sorry about that. forget my previous comment. It's a totally different issue

  8. DanielZlotin commented on Oct 19, 2016

    @DanielZlotin
    ContributorAuthor

    A prop specifying throttle of some kind is a possible solution IMO.
    It's really easy creating a library that wraps the default TouchableX with another prop. However, I'm not sure this is the best solution...

  9. lacker commented on Dec 17, 2016

    @lacker
    Contributor

    Since there are some workarounds here I am going to close the issue. Since this isn't totally specific to React Native development I don't know if we'll ever have a great fix.

  10. jdmunro commented on Jan 5, 2017

    @jdmunro
    Contributor

    If anyone is interested, I've used this 'rough' solution with some success:

    class Touchable extends Component {
    
      constructor(props) {
        super(props);
        this.onPress = debounce(this.onPress.bind(this), 1000, {
          leading: true,
          trailing: false,
        });
      }
    
      onPress() {
        if (this.props.onPress) {
          this.props.onPress();
        }
      }
    
      render() {
        return (
          <TouchableOpacity
            {...this.props}
            onPress={this.props.skipDebounce ? this.props.onPress : this.onPress}
          />
        );
      }
    }
    

    It's not ideal but any means but it solved the most common problems I encountered.

  11. locked as resolved and limited conversation to collaborators on Jul 19, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions