Evanss
Evanss

Reputation: 23563

Execute a function in a child component when a scroll event occurs in a parent in React?

My project is React Native but this is a general React question. I need to get the Y position of a View relative to the root element (so I can do some calculations against the pageY event from a PanResponder). This View is in a parent component with a ScrollView and I need this value to update as you scroll as it will have changed.

I can do the measurement in the View with onLayout and the measure callback so my question is how should I recalculate when scrolling occurs? I have a solution that works but I'm not sure if it's ideal in terms of performance or if I've gone about it in a needlessly verbose way.

My solution is to use the onScroll event to set a piece of state which I pass down to the View, and then use it as a dependancy in useEffect which re-fires the calculation. Im not sure if it's efficient as the View component doesn't actually use this value, it only needs the event that the scroll position has changed. So the View is currently re-rendering every-time you scroll:

export default function App() {
  const [scrollPosition, setScrollPosition] = React.useState(null);

  return (
    <ScrollView
      onScroll={(e) => {
        setScrollPosition(e.nativeEvent.contentOffset.y);
      }}
      scrollEventThrottle={16}
    >
      <Text>Text</Text>
      <Text>Text</Text>
      <Text>Text</Text>
      <Text>Text</Text>
      <Text>Text</Text>
      <Text>Text</Text>
      <Text>Text</Text>
      <ContainerFunctional scrollPosition={scrollPosition} />
    </ScrollView>
  );
}

const ContainerFunctional = ({ scrollPosition }) => {
  const items = new Array(100).fill("");
  const marker = React.useRef(null);

  React.useEffect(() => {
    marker.current.measure((x, y, width, height, pageX, pageY) => {
      if (scrollPosition !== null) {
        console.log("on scroll ", pageY);
      }
    });
  }, [scrollPosition]);

  return (
    <View
      ref={marker}
      onLayout={() => {
        if (marker.current) {
          marker.current.measure((x, y, width, height, pageX, pageY) => {
            console.log("initial ", pageY);
          });
        }
      }}
    >
      {items.map((item, index) => {
        return <Text key={index}>{index}</Text>;
      })}
    </View>
  );
};

Upvotes: 1

Views: 3045

Answers (1)

Tiberiu Maran
Tiberiu Maran

Reputation: 2173

One solution to avoid re-rendering is to use a ref.

export default function App() {
  const container = React.useRef(null);

  return (
    <ScrollView
      onScroll={(e) => {
        container.current.onScroll();
      }}
      scrollEventThrottle={16}
    >
      <Text>Text</Text>
      <Text>Text</Text>
      <Text>Text</Text>
      <Text>Text</Text>
      <Text>Text</Text>
      <Text>Text</Text>
      <Text>Text</Text>
      <ContainerFunctional ref={container} />
    </ScrollView>
  );
}

const ContainerFunctional = React.forwardRef((_, ref) => {
  const items = new Array(100).fill("");
  const marker = React.useRef(null);

  React.useImperativeHandle(ref, () => ({
    onScroll: () =>{
      marker.current.measure((x, y, width, height, pageX, pageY) => {
        console.log("on scroll ", pageY);
      });
    }
  }));

  return (
    <View
      ref={marker}
      onLayout={() => {
        if (marker.current) {
          marker.current.measure((x, y, width, height, pageX, pageY) => {
            console.log("initial ", pageY);
          });
        }
      }}
    >
      {items.map((item, index) => {
        return <Text key={index}>{index}</Text>;
      })}
    </View>
  );
});

Upvotes: 5

Related Questions