Skip to content

Add ScrollView interceptor directly to children#4331

Open
m-bert wants to merge 2 commits into
mainfrom
@mbert/fix-sticky-scroll
Open

Add ScrollView interceptor directly to children#4331
m-bert wants to merge 2 commits into
mainfrom
@mbert/fix-sticky-scroll

Conversation

@m-bert

@m-bert m-bert commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Description

#4158 introduced ScrollViewResponderInterceptor, which wraps all ScrollView children in a single logical-responder View. RN's ScrollView.render() resolves stickyHeaderIndices against React.Children.toArray(this.props.children) and wraps the child at each sticky index in ScrollViewStickyHeader, which counter-translates that child against the scroll offset. With the content collapsed into one child, index 0 is the entire content — so everything gets pinned. Indices > 0 were a silent no-op (they pointed past the single child).

To fix this, we now wrap each child separately.

Fixes #4328

Test plan

Tested on the following code:
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { FlatList, LegacyFlatList } from 'react-native-gesture-handler';

// Repro for https://github.com/software-mansion/react-native-gesture-handler/issues/4328
// v3 FlatList with stickyHeaderIndices: scroll indicator moves, content doesn't.
// LegacyFlatList behaves correctly.

const items = Array.from({ length: 50 }, (_, i) => ({
  id: i.toString(),
  text: `Text content for ${i}`,
}));

export default function EmptyExample() {
  const renderItem = ({ item }: { item: (typeof items)[0] }) => (
    <Text style={styles.item}>{item.text}</Text>
  );

  return (
    <View style={styles.container}>
      <View style={styles.flex1}>
        <FlatList
          data={items}
          keyExtractor={(item) => item.id}
          contentContainerStyle={styles.contentContainer}
          stickyHeaderHiddenOnScroll={false}
          ListHeaderComponentStyle={styles.headerContainer}
          stickyHeaderIndices={[0]}
          ListHeaderComponent={<Text>FlatList (v3) Header</Text>}
          renderItem={renderItem}
        />
      </View>
      <View style={styles.flex1}>
        <LegacyFlatList
          data={items}
          keyExtractor={(item) => item.id}
          contentContainerStyle={styles.contentContainer}
          stickyHeaderHiddenOnScroll={false}
          ListHeaderComponentStyle={styles.headerContainer}
          stickyHeaderIndices={[0]}
          ListHeaderComponent={<Text>LegacyFlatList Header</Text>}
          renderItem={renderItem}
        />
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  flex1: {
    flex: 1,
  },
  headerContainer: {
    paddingTop: 24,
    paddingBottom: 12,
    backgroundColor: '#f6f6f6',
  },
  contentContainer: {
    paddingHorizontal: 24,
  },
  item: {
    paddingVertical: 4,
  },
});

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a regression in v3 ScrollView/FlatList sticky header behavior introduced by wrapping all ScrollView children into a single logical responder node, which caused stickyHeaderIndices to pin the entire content (or become a no-op for indices > 0). The fix wraps each child individually while preserving the responder-interception behavior via a shared context provider.

Changes:

  • Replace the single “logical responder” wrapper with per-child logical wrappers (interceptScrollViewChildren) so stickyHeaderIndices map to the intended children.
  • Split the interceptor into a context provider (ScrollViewResponderProvider) plus per-child wrappers (LogicalResponderChild), and apply this in the v3 ScrollView component.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
packages/react-native-gesture-handler/src/v3/components/ScrollViewResponderInterceptor.tsx Refactors responder interception into a provider + per-child wrapper and adds interceptScrollViewChildren.
packages/react-native-gesture-handler/src/v3/components/GestureComponents.tsx Updates v3 ScrollView to use the provider and wrap children individually via interceptScrollViewChildren.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +113 to +115
return (
<JSResponderContext value={contextValue}>{children}</JSResponderContext>
);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think React <19 is a concern

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
{children}
</View>
</JSResponderContext>
<View

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're rendering a new non-flattenable view for each ScrollView child. This will measurably impact performance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[v3] Regression: FlatList content doesn't scroll when stickyHeaderIndices is set, does not affect LegacyFlatlist

3 participants