Home > Mobile >  ScrollView Pagination causes all children to re-render
ScrollView Pagination causes all children to re-render

Time:12-28

Problem:

When using a ScrollView in ReactNative for horizontal pagination it re-renders all children, but I would like to keep the state values of certain local input fields and local variables of children components.

In the code below, if I were in the middle of updating a TextInput within the NotesSection but wanted to swipe back to the BatchSection to review some metadata, the code re-renders NotesSection and resets a local state holding the text value.

Diagnosis:

I'm very new to React and React Native, but my best guess here is that this happens due to the parent state variable "horizontalPos" which takes an integer to reflect what page is in focus. This is simply used in the ProductHeader component to highlight a coloured bottomBorder showing the user a kind of small "menu" at the top of the screen. The "horizontalPos" state can be updated in 2 ways:

  1. First one is simply when clicking the wanted header (TouchableOpacity) within ProductHeader which triggers a state change and uses useRef to automatically move the ScrollView.
  2. Second option is when the user swipes on the ScrollView. Using OnScroll to run a function "handleHorizontalScroll" which in turn sets the "horizontalPos" state using simple maths from the contentOffset.x.

Question / Solution:

If "horizontalPos" state was INSIDE ProductHeader I suspect this would solve the issue but I can't wrap my mind around how to do this as I don't believe it's possible to pass a function through to the child based on a change in the parent component.

I'm dependent on registering the OnScroll on the main ScrollView and the remaining components likewise have to be inside the main ScrollView but I don't want them to re-render every time the "horizontalPos" state updates.

Code:

const ProductScreen = (props) => {
    const [horizontalPos, setHorizontalPos] = useState(0)
  
    const scrollRef = useRef()

    const toggleHorizontal = (page) => {
        setHorizontalPos(page)
        scrollRef.current.scrollTo({x:page*width, y:0, animated:false})
    }

    const handleHorizontalScroll = (v) => {
        const pagination = Math.round(v.nativeEvent.contentOffset.x / width)
        if (pagination != horizontalPos){
          setHorizontalPos(pagination)
        }
    }

    const ProductHeader = () => {
        return(
            <View style={styles.scrollHeaderContainer}>
                <TouchableOpacity style={[styles.scrollHeader, horizontalPos == 0 ? {borderColor: AppGreenDark,} : null]} onPress={() => toggleHorizontal(0)}>
                    <Text style={styles.scrollHeaderText}>Meta Data</Text>
                </TouchableOpacity>
                
                <TouchableOpacity style={[styles.scrollHeader, horizontalPos == 1 ? {borderColor: AppGreenDark,} : null]} onPress={() => toggleHorizontal(1)}>
                    <Text style={styles.scrollHeaderText}>{"Notes"}</Text>
                </TouchableOpacity>
            </View>
        )
    }

    return (
        <View style={styles.container}>
            <ProductHeader/>

            <ScrollView
                ref={scrollRef}
                decelerationRate={'fast'}
                horizontal={true}
                showsHorizontalScrollIndicator={false}
                snapToInterval={width}
                onScroll={handleHorizontalScroll}
                scrollEventThrottle={16}
                disableIntervalMomentum={true}
                style={{flex: 1}}
            >
                <View style={[styles.horizontalScroll]}>
                    <View style={styles.mainScrollView}>
                        <BatchSection/>
                    </View>

                    <ScrollView style={styles.notesScrollView}>
                        <NotesSection/>
                    </ScrollView>
                </View>

            </ScrollView>
        </View>
    )
}

CodePudding user response:

As you outlined, updating horizontalPos state inside ProductScreen will cause a whole screen to re-render which is not an expected behavior.

To avoid this scenario, let's refactor the code as below:

function debounce(func, timeout = 500){
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => { func.apply(this, args); }, timeout);
  };
}

class ProductHeader extends React.Component  {
  state = {horizontalPos:0 }
   toggleHorizontal = (page) => {
      this.setState({horizontalPos:page});
      this.props.onPositionChange(page);
    };


render () {
  const {horizontalPos} = this.state
   return (
      <View style={styles.scrollHeaderContainer}>
        <TouchableOpacity
          style={[
            styles.scrollHeader,
            horizontalPos == 0 ? { borderColor: AppGreenDark } : null,
          ]}
          onPress={() => this.toggleHorizontal(0)}
        >
          <Text style={styles.scrollHeaderText}>Meta Data</Text>
        </TouchableOpacity>

        <TouchableOpacity
          style={[
            styles.scrollHeader,
            horizontalPos == 1 ? { borderColor: AppGreenDark } : null,
          ]}
          onPress={() => this.toggleHorizontal(1)}
        >
          <Text style={styles.scrollHeaderText}>{"Notes"}</Text>
        </TouchableOpacity>
      </View>
    );
  
}

   
  };




const ProductScreen = (props) => {
  const scrollRef = useRef();
  const productHeaderRef = useRef()
  let horizontalPos = 0;

  const handleHorizontalScroll = (v) => {
    const pagination = Math.round(v.nativeEvent.contentOffset.x / width);
    if (pagination != horizontalPos) {
      productHeaderRef.current?.toggleHorizontal(pagination)
    }
  };
  
    const debouncedHorizontalScroll= debounce(handleHorizontalScroll,500)

  const onPositionChange = (page) => {
    horizontalPos = page;
    scrollRef.current.scrollTo({ x: page * width, y: 0, animated: false });
  };

  return (
    <View style={styles.container}>
      <ProductHeader onPositionChange={onPositionChange} ref={productHeaderRef} />

      <ScrollView
        ref={scrollRef}
        decelerationRate={"fast"}
        horizontal={true}
        showsHorizontalScrollIndicator={false}
        snapToInterval={width}
        onScroll={debouncedHorizontalScroll}
        scrollEventThrottle={16}
        disableIntervalMomentum={true}
        style={{ flex: 1 }}
      >
        <View style={[styles.horizontalScroll]}>
          <View style={styles.mainScrollView}>
            <BatchSection />
          </View>

          <ScrollView style={styles.notesScrollView}>
            <NotesSection />
          </ScrollView>
        </View>
      </ScrollView>
    </View>
  );
};



I hope this will stop the whole screen from rerendering and maintaining pagination.

  • Related