Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React Native: Rendering infinite number of pages (bi-directional swiping)

Question

How do people implement an infinite number of pages for you to swipe through dynamically?

Details

If an app renders a page for a day you can expect to swipe to the right for the previous day and to the left for the next day.

For example, the FitNotes app does this seemlessly: FitNotes app day view

Swiping to the right/left is the same as pressing on button 5/6. and displays already rendered pages. At the same time new pages for the now adjacent days are rendered as well.

Initially, I thought that this is something you would do using a carousel e.g., using the react-native-snap-carousel library. I imagined something like the example below but rendering a page using state variables instead of an array of images.

Example carousel from the react-native-snap-carousel library

So, if I wanted to render a page with the date at the top, I would have an array as part of the state, for example:

carouselElements: [ '2019/01/21', '2019/01/22', '2019/01/23']

Fed into the carousel below, I would get back three pages each one with the date rendered at the top.

<Carousel
    ref={(c) => { this._carousel = c; }}
    data={this.props.carouselElements}
    renderItem={this.renderDay}
    sliderWidth={sliderWidth}
    itemWidth={sliderWidth}
    enableSnap 
    firstItem={1}
    initialNumToRender={3}
    maxToRenderPerBatch={3}
    onBeforeSnapToItem={(index) => this.changeEvent(index)}
    useScrollView/>

Problem 1: Infinitely growing array

You can start at Day X and render the adjacent two days ahead of time. If I swipe to the left, you access Day X+1 and concat Day X+2 to the end of the array and render that day as well.

The array becomes: [ 'Day X-1', 'Day X', 'Day X+1', 'Day X+2']

For performance, I can keep checking the index against a 3 day window and only render the selected day and the adjacent two days. But the array will grow infinitely and your code is O(n) at best.

Problem 2: Indexing

a) Prepending elements:

You can start at Day X and and then swipe to the right. You are now showing Day X-1. You add Day X-2 to the start of the array and render it to prepare for a second swipe to the right. The problem is you added an element at index 0. Your system is pointing at the state of index 0 when it changed from Day X-1 to Day X-2, thus the page now shows Day X-2 instead.

i.e., instead of moving back one day when you swiped to the right, you moved back 2 days with each swipe.

b) Removing elements

If I remove an element to keep the array small, everything shifts but the index remains the same. The index now points to a different element than the intended one in the array and this reflects in the rendered page before the index can be corrected.

Thus my question: How do people implement dynamic page rendering ad infinitum with page swiping? It seems like a lot of apps have it and I can't find anyone talking about it.


Edits

EDIT 1: Added images with accompanying edits to explain my problem better.

EDIT 2: I have a working implementation with react-native-swiper that basically takes a massive array and only renders the batch of slides immediately adjacent to the displayed page.

<Swiper style={styles.wrapper} 
          showsButtons={false}
          index={currentDateIndex}
          loadMinimal={true}
          loadMinimalSize={1}
          showsPagination={false}
          loop={false}
          onIndexChanged={(index) => dispatch(changeSelectedDayByIndex(index))}
         >
          {dates.map((item) => 
              this.renderDay(item)
            )
          }
</Swiper>

However, the dates.map command imposes a large performance penalty at startup.

Edit 3: Using a slice command on the dates array before mapping doesn't seem like an option because I'd be rendering during an existing state transition..

like image 752
HBSKan Avatar asked Jan 22 '19 22:01

HBSKan


1 Answers

This solution is based on this react-native-swiper you mentioned. This is a tricky solution that can work if caching is done properly. It's based on the following assumptions:

  • Given an ID, you can derive ID+1 and ID-1. (This is valid for dates)
  • You can scroll left or right only by 1 frame(View).

The idea is to have a wrapper component around the <Swiper> to hold an state array with 3 IDs: current, left and right.

state = {
  pages: ["2", "3", "4"],
  key: 1
}

When you swipe left or right, you can derive the next or previous 3 IDs and call setState. The render method would call the local cached information to get the view attributes for this 3 IDs and render the new Screens. The <Swiper> element always have to point to index={1} as initial param, and keep it this way.

const cache = {
  "0": "zero",
  "1": "one",
  "2": "two",
  "3": "three",
  "4": "four",
  "5": "five",
  "6": "six",
  "7": "seven",
  "8": "eight",
  "9": "nine",
  "10": "ten"
}

renderItem(item, idx) {
    const itemInt = parseInt(item)
    const style = itemInt % 2 == 0 ? styles.slide1 : styles.slide2
    return (
      <View style={style} key={idx}>
        <Text style={styles.text}>{cache[item]}</Text>
      </View>
    )
  }

However, when you swipe, it will set the current index to 2 or 0. So in order to make it to always point to index 1 (again, because you shifted and reloaded), you have to force the <Swiper> element to reload. You can do that by setting its key attribute to be defined by a state variable that always changes. That's the tricky part.

onPageChanged(idx) {
    if (idx == 2) {
                                                 //deriving ID+1
      const newPages = this.state.pages.map(i => (parseInt(i)+1).toString())
      this.setState({pages: newPages, key: ((this.state.key+1)%2) })
    } else if (idx == 0) {
                                                 //deriving ID-1
      const newPages = this.state.pages.map(i => (parseInt(i)-1).toString())
      this.setState({pages: newPages, key: ((this.state.key+1)%2) })
    }
  }

render() {
    return (
      <Swiper
        index={1}
        key={this.state.key}
        style={styles.wrapper}
        loop={false}
        showsPagination={false}
        onIndexChanged={(index) => this.onPageChanged(index)}>
        {this.state.pages.map((item, idx) => this.renderItem(item, idx))}
      </Swiper>
    );
  }

Aside all that, you have to make sure to always have enough cached information for your IDs. If the cache access is getting close to end, make sure to request new data and delete some amount of cached data in the other direction.

Here's a PasteBin for the App.js and a GitHub project so you can test the whole idea.

enter image description here

like image 145
diogenesgg Avatar answered Oct 19 '22 21:10

diogenesgg