React Native

How Insidious React Native onPress Could Be

Story of a surprising bug

Imagine, you are working on a social network application. You implement a button to create a new post. Your code would probably be like the following:

++pre>++code>...
const createPost = () => {
 displayPostCreationModal();
}
...
<NewPostButton onPress={createPost} />
...
++/code>++/pre>

Then you decide to implement a post sharing. The share button can use the same function, we will just slightly modify it:

++pre>++code><ShareButton onPress={() => createPost(sharedPost)} />
++/code>++/pre>++pre>++code>const createPost = (sharedPost?) => {
 if (sharedPost) {
   displayPostCreationModal(sharedPost.message);
 }
 else {
   displayPostCreationModal();
 }
}
++/code>++/pre>

The implemented functionality works fine: when you press the share button, you see a modal with a pre-filled message.

Everything looks fine until the moment you press again the button creating a new post from scratch...

++table cellpadding="4" style="width: 100%; border-collapse: collapse; table-layout: fixed; border: 1px hidden #FFFFFF; border-width: 0px; border-style: none;">++tbody>++tr>++td style="width: 19.7863%;">

++/td>++td style="width: 86.2356%;"> undefined is not an object (evaluating 'sharedPost.message.length')++/td>++/tr>++/tbody>++/table>

The appeared error is confusing because the only place you try to access sharedPost.message is the if scope of the createPost function. While when we create a new post... we do not provide any parameter ?

Indeed, if you place breakpoints at the beginning of each scope and press the NewPostButton, you will stop in the if scope and see your sharedPost be an object with not really expected content: {"_dispatchInstances": {"_debugHookTypes": null, "_debugIsCurrentlyTiming": false, ... (it is long, I will not fully present the object here, but there is nothing about shared post or its message)


BUG REASON

I will avoid beating around the bush and come straight to the point.

The problem comes from the "secret" onPress parameter.

Here is the ReactNative onPress type: ((event: GestureResponderEvent) => void) | undefined.

Back to the example: we were actually trying to execute createPost(event). The event parameter was thereby treated as a sharedPost , which provoked the error crashing our application.


What is the Press Event about?

++code>PressEvent++/code> object is returned in the callback as a result of user press interaction, here is the documentation.

The ++code>onPress++/code> property of many React Native core components uses the event argument in form of ++code>PressEvent++/code>: ++code>Button++/code>, ++code>TouchableOpacity, TouchableWithoutFeedback,++/code> ++code>Pressable++/code>, etc. The situation is the same with ++code>onPressIn ++/code>and ++code>onPressOut++/code> props that some of listed components have.

The PressEvent ++code>++/code> is mainly used inside the React Native code for managing the behavior of different touchables. If you have a real-live example of its utilization, do not hesitate to share it ?


How to resolve the problem?

To resolve the problem, we can simply modify onPress of the button creating a new post: precise that createPost function should be called without parameters like below:

++pre>++code><NewPostButton onPress={() => createPost()} />++/code>++code>++/code>++/pre>


How to prevent the problem?

As any bug, even more than know how to fix it, we would rather not have it at all ?

So, here are some steps that could prevent its introduction.

  • At first, be attentive with types.
  • I didn't use { () => createPost() } definition initially because it seemed to me redundant and less efficient.
  • Why hadn't I seen the typescript error? Because of my custom button type, which was the following: NewPostButton: FunctionComponent<{ onPress: () => void }>
  • We had simplified onPress type making it incorrect. I do not want to recommend using the ((event: GestureResponderEvent) => void) | undefined type all the time, because extra information makes your code less readable. Be attentive though.

    Another option is to create a custom <TouchableOpacity onPress={() ? onPress()} /> and forbid the TouchableOpacity import from 'react-native' with an eslint rule.
  • Secondly, always retest manually the functionalities that your new code could impact. As obvious it seems to be, sometimes we are too lazy to actually do it. Aren't you? ?
  • Check all components that use modified variables and functions to not forget something. The code editors usually propose a "++code>Find all references"++/code> functionality, I advise you to memorize its shortcut (for example, ++code>??F12++/code> in Visual Studio Code) to easily find all impacted code.
  • Thirdly, the problem could be caught with a simple integration test. Read this article to learn how to write tests independent of the implementation with react-native-testing-library.

Développeur mobile ?

Rejoins nos équipes