react native Why does my async code hang the rendering?

Viewed 161

General :

TL;DR: async code hangs rendering.

I have this component with a Modal and inside the Modal it renders a list of filters the user can choose from. When pressing a filter the color of the item changes and it adds a simple code(Number) to an array. The problem is that the rendering of the color change hangs until the logic that adds the code to the array finishes.

I don't understand why adding a number to an array takes between a sec and two. I don't understand why the rendering hangs until the entire logic behind is done.

Notes: I come from a Vue background and this is the first project where I'm using react/react-native. So if I'm doing something wrong it would be much appreciated if someone points that out

Snack that replicates the issue :

Snack Link

My code for reference :

I use react-native with expo managed and I use some native-base components for the UI. I can't share the whole code source but here are the pieces of logic that contribute to the problem :

Parent : FilterModal.js

The rendering part :

...
<Modal
  // style={styles.container}
  visible={modalVisible}
  animationType="slide"
  transparent={false}
  onRequestClose={() => {
  this.setModalVisible(!modalVisible);
  }}
>
  <Center>
    <Pressable
      onPress={() => this.setModalVisible(!modalVisible)}
    >
      <Icon size="8" as={MaterialCommunityIcons} name="window-close" color="danger.500" />
    </Pressable>
  </Center>
  // I use sectionList because the list of filters is big and takes time to render on the screen
  <SectionList
    style={styles.container}
    sections={[
      { title: "job types", data: job_types },
      { title: "job experience", data: job_experience },
      { title: "education", data: job_formation },
      { title: "sector", data: job_secteur }
    ]}
    keyExtractor={(item) => item.id}
    renderItem={({ item, section }) => <BaseBadge 
                                         key={item.id} 
                                         pressed={this.isPressed(section.title, item.id)} 
                                         item={item.name} 
                                         code={item.id} 
                                         type={section.title} 
                                         add={this.addToFilters.bind(this)}
                                       ></BaseBadge>}
    renderSectionHeader={({ section: { title } }) => (
                                            <Heading color="darkBlue.400">{title}</Heading>
                                        )}
  />
</Modal>
...

The logic part :

...
async addToFilters(type, code) {
  switch (type) {
  case "job types":
    this.addToTypesSelection(code);
    break;
  case "job experience":
    this.addToExperienceSelection(code);
    break;
  case "formation":
    this.addToFormationSelection(code);
    break;
  case "sector":
    this.addToSectorSelection(code);
    break;

  default:
    //TODO
    break;
  }
}
...
// the add to selection methods look something like this :
async addToTypesSelection(code) {
  if (this.state.jobTypesSelection.includes(code)) {
    this.setState({ jobTypesSelection: this.state.jobTypesSelection.filter((item) => item != code) })
  }
  else {
    this.setState({ jobTypesSelection: [...this.state.jobTypesSelection, code] })
  }
}
...

Child :

The rendering Part

render() {
  const { pressed } = this.state;
  return (
    < Pressable
      // This is the source of the problem and read further to know why I used the setTimeout
      onPress={async () => {
        this.setState({ pressed: !this.state.pressed });
        setTimeout(() => {
          this.props.add(this.props.type, this.props.code);
        });
      }}
    >
      <Badge
        bg={pressed ? "primary.300" : "coolGray.200"}
        rounded="md"
      >
        <Text fontSize="md">
          {this.props.item}
        </Text>
      </Badge>
    </Pressable >
  );
};

Expected outcome :

The setState({pressed:!this.state.pressed}) finishes the rendering of the item happens instantly, the rest of the code happens after and doesn't hang the rendering.

The change in the parent state using the add code to array can happen in the background but I need the filter item ui to change instantly.

Things I tried :

Async methods

I tried making the methods async and not await them so they can happen asynchronously. that didn't change anything and seems like react native ignores that the methods are async. It hangs until everything is done all the way to the method changing the parent state.

Implementing "event emit-listen logic"

This is the first app where I chose to use react/react-native, coming from Vue I got the idea of emitting an event from the child and listening to it on the parent and execute the logic that adds the code to the array.

This didn't change anything, I used eventemitter3 and react-native-event-listeners

Using Timeout

This is the last desperate thing I tried which made the app useable for now until I figure out what am I doing wrong. basically I add a Timeout after I change the state of the filter component like so :

...
< Pressable
  onPress={async () => {
    // change the state this changes the color of the item ↓
    this.setState({ pressed: !this.state.pressed });
    // this is the desperate code to make the logic not hang the rendering ↓
    setTimeout(() => {
      this.props.add(this.props.type, this.props.code);
    });
  }}
>
...

Thanks for reading, helpful answers and links to the docs and other articles that can help me understand better are much appreciated.

Again I'm new to react/react-native so please if there is some concept I'm not understanding right point me in the right direction.

1 Answers

For anyone reading this I finally figured out what was the problem and was able to solve the issue for me.

The reason the rendering was getting hang is because the code that pushes to my array took time regardless of me making it async or not it was being executed on the main thread AND that change was triggering screen re-render which needed to wait for the js logic to finish.

The things that contribute to the solution and improvement are :

  • Make the array (now a map{}) that holds the selected filters stateless, in other words don't use useState to declare the array, instead use good old js which will not trigger any screen re-render. When the user applies the filters then push that plain js object to a state or context like I'm doing and consume it, doing it this way makes sure that the user can spam selecting and deselecting the filters without hanging the interactions.
  • first thing which is just a better way of doing what I needed is to make the array a map, this doesn't solve the rerender issue.
Related