{"id":24995315,"url":"https://github.com/launchplatform/inverted-section-list","last_synced_at":"2025-07-20T16:36:07.728Z","repository":{"id":46269392,"uuid":"423281417","full_name":"LaunchPlatform/inverted-section-list","owner":"LaunchPlatform","description":"A React Native component that implements SectionList with inverted direction and working sticky header","archived":false,"fork":false,"pushed_at":"2022-09-23T04:04:02.000Z","size":9784,"stargazers_count":23,"open_issues_count":3,"forks_count":4,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-03-25T23:41:47.734Z","etag":null,"topics":["react-native","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/LaunchPlatform.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-10-31T23:14:23.000Z","updated_at":"2023-12-08T12:49:21.000Z","dependencies_parsed_at":"2022-09-26T22:20:24.246Z","dependency_job_id":null,"html_url":"https://github.com/LaunchPlatform/inverted-section-list","commit_stats":null,"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LaunchPlatform%2Finverted-section-list","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LaunchPlatform%2Finverted-section-list/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LaunchPlatform%2Finverted-section-list/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LaunchPlatform%2Finverted-section-list/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/LaunchPlatform","download_url":"https://codeload.github.com/LaunchPlatform/inverted-section-list/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248514221,"owners_count":21116903,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["react-native","typescript"],"created_at":"2025-02-04T15:35:09.796Z","updated_at":"2025-04-12T04:11:02.261Z","avatar_url":"https://github.com/LaunchPlatform.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# InvertedSectionList\nA React Native component that implements SectionList with inverted direction and working sticky header\n\n# Supported React Native version\n\nCurrently we support 66 or above, however, new changes introduce in the future might break it at somepoint.\n\n# Demo\n\n```typescript\nimport React, { FunctionComponent } from \"react\";\nimport {\n  SafeAreaView,\n  StyleSheet,\n  SectionList,\n  Text,\n  View,\n} from \"react-native\";\nimport InvertedSectionList from \"inverted-section-list\";\n\nconst styles = StyleSheet.create({\n  container: {\n    flex: 1,\n    marginHorizontal: 16,\n  },\n  item: {\n    backgroundColor: \"#f9c2ff\",\n    padding: 20,\n    marginVertical: 8,\n  },\n  header: {\n    fontSize: 32,\n    backgroundColor: \"#fff\",\n  },\n  title: {\n    fontSize: 24,\n  },\n});\n\nconst DATA = [\n  {\n    title: \"Main dishes\",\n    data: [\"Pizza\", \"Burger\", \"Risotto\", \"a\", \"b\", \"c\", \"1\", \"2\", \"3\"],\n  },\n  {\n    title: \"Sides\",\n    data: [\"French Fries\", \"Onion Rings\", \"Fried Shrimps\"],\n  },\n  {\n    title: \"Drinks\",\n    data: [\"Water\", \"Coke\", \"Beer\"],\n  },\n  {\n    title: \"Desserts\",\n    data: [\"Cheese Cake\", \"Ice Cream\"],\n  },\n];\n\nconst Item = ({ title }: { title: string }) =\u003e (\n  \u003cView style={styles.item}\u003e\n    \u003cText style={styles.title}\u003e{title}\u003c/Text\u003e\n  \u003c/View\u003e\n);\n\nconst App: FunctionComponent = () =\u003e (\n  \u003cSafeAreaView style={styles.container}\u003e\n    \u003cInvertedSectionList\n      sections={DATA}\n      keyExtractor={(item, index) =\u003e item + index}\n      renderItem={({ item }) =\u003e \u003cItem title={item} /\u003e}\n      renderSectionFooter={({ section: { title } }) =\u003e (\n        \u003cText style={styles.header}\u003e{title}\u003c/Text\u003e\n      )}\n      stickySectionHeadersEnabled\n    /\u003e\n  \u003c/SafeAreaView\u003e\n);\n\nexport default App;\n```\n\nTo run the demo:\n\n```bash\ncd example\nyarn install --dev\nyarn start\n```\n\n# Install\n\nRun\n\n```bash\nyarn add inverted-section-list\n```\n\n# Why?\n\nThe sticky header of inverted SectionList component of React Native is not working as expected.\nThere was an issue [#18945](https://github.com/facebook/react-native/issues/18945) open for years but no sign of the problem been fixed.\nAt Launch Platform, we are building a app product [Monoline](https://monoline.io), and its message list needs to present in inverted direction\nwith working sticky header:\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"assets/monoline-demo.gif?raw=true\" alt=\"Monoline demo screencast\" /\u003e\n\u003c/p\u003e\n\nWe have no choice but to find a way to fix this problem. Forking React Native is too much effort for us\nto maintain. We have plan to open Pull Requests to upstream React Native repository, but we anticipate those\nwill take long time before they got reviewed and merged. To solve the problem before it's fixed in the upstream,\nwe build a standalone `InvertedSectionList` component.\n\nWe thought others in the community could benefit from this, so we open sourced it here.\n\n# How?\n\nThere are different places where the logic needed to be changed in order for the inverted sticky header to work.\nBut those logic are deeply baked inside the build-in component's source code and there's no easy way to change them\nfrom the outside. In order to make our InvertedSectionList component's sticky header to work, we copied the source\ncode from React Native 0.64 for following components:\n\n- [ScrollView](https://github.com/facebook/react-native/blob/757bb75fbf837714725d7b2af62149e8e2a7ee51/Libraries/Components/ScrollView/ScrollView.js)\n- [ScrollViewStickyFooter](https://github.com/facebook/react-native/blob/6790cf137f73f2d7863911f9115317048c66a6ee/Libraries/Components/ScrollView/ScrollViewStickyHeader.js)\n- [VirtualizedSectionList](https://github.com/facebook/react-native/blob/6790cf137f73f2d7863911f9115317048c66a6ee/Libraries/Lists/VirtualizedSectionList.js)\n\nSince we are using TypeScript here, so the original source code are converted into TypeScript.\nThere are following key changes were made from the original source code.\n\n## ScrollView\n\nFor the `StickyHeaderComponent` component, we don't just pass in `nextHeaderLayoutY`, since now the order is inverted, we need to\nalso pass in `prevHeaderLayoutY` for the next sticky header to calculate the correct position of begin and end.\nSuch as, the sticky header layout update callback needs to set prev header value [here](https://github.com/LaunchPlatform/inverted-section-list/blob/db04f829993f0e1c6f6ba261fb459f8264080466/src/ScrollView.tsx#L446-L454):\n\n```typescript\nprivate _onStickyHeaderLayout(\n  index: number,\n  event: LayoutChangeEvent,\n  key: string\n) {\n  /* ... */\n  const nextHeaderIndex = stickyHeaderIndices[indexOfIndex + 1];\n  if (nextHeaderIndex != null) {\n    const nextHeader = this._stickyHeaderRefs.get(\n      this._getKeyForIndex(nextHeaderIndex, childArray)\n    );\n    nextHeader \u0026\u0026\n      (nextHeader as any).setPrevHeaderY \u0026\u0026\n      (nextHeader as any).setPrevHeaderY(layoutY + height);\n  }\n}\n```\n\nAnd extra `prevLayoutY` props value needs to be calculated [here](https://github.com/LaunchPlatform/inverted-section-list/blob/db04f829993f0e1c6f6ba261fb459f8264080466/src/ScrollView.tsx#L572-L578):\n\n```typescript\nconst prevKey = this._getKeyForIndex(prevIndex, childArray);\nconst prevLayoutY = this._headerLayoutYs.get(prevKey);\nconst prevLayoutHeight = this._headerLayoutHeights.get(prevKey);\nlet prevHeaderLayoutY: number | undefined = undefined;\nif (prevLayoutY != null \u0026\u0026 prevLayoutHeight != null) {\n  prevHeaderLayoutY = prevLayoutY + prevLayoutHeight;\n}\n```\n\nThen passed into the `StickyHeaderComponent` [here](https://github.com/LaunchPlatform/inverted-section-list/blob/db04f829993f0e1c6f6ba261fb459f8264080466/src/ScrollView.tsx#L588)\n\n## StickyFooterComponent\n\nThe `StickyHeaderComponent` source code is copied and renamed as `StickyFooterComponent`, because to make\nsticky \"header\" works, we pass the header component as footer instead. New method `setPrevHeaderY` is\nadded [here](https://github.com/LaunchPlatform/inverted-section-list/blob/ceb0d30fbb50552f3037fb76d78fd46e37536da6/src/ScrollViewStickyFooter.tsx#L72-L75)\nto receivew the previous header's position from `ScrollView`.\n\nThe another major change [here](https://github.com/LaunchPlatform/inverted-section-list/blob/ceb0d30fbb50552f3037fb76d78fd46e37536da6/src/ScrollViewStickyFooter.tsx#L210-L231)\nis implementing the correct position calculation logic with the preview header y position provided from\nour own `ScrollView`:\n\n```typescript\n// The interpolate looks like this:\n//\n// ------\n//       \\\n//        \\            height = delta\n//         \\---------\n//  prev^   ^current\n//        ^ width = delta\n//\n// Basically, it starts from `prevStickEndPoint`, where the\n// previous header stops scrolling. Then we starts the sticking by adding\n// negative delta to the `translateY` to cancel the scrolling offset.\n// Until the point, where we have scroll to where the current header's original\n// position, at this point the `translateY` goes down to 0 so that it\n// will scroll with the content\ninputRange = [\n  prevStickEndPoint - 1,\n  prevStickEndPoint,\n  stickStartPoint,\n  stickStartPoint + 1,\n];\noutputRange = [-delta, -delta, 0, 0];\n```\n\n## InvertedSectionList\n\nWe copied and combined the\n[VirtualizedSectionList](https://github.com/facebook/react-native/blob/6790cf137f73f2d7863911f9115317048c66a6ee/Libraries/Lists/VirtualizedSectionList.js) and\n[SectionList](https://github.com/facebook/react-native/blob/6790cf137f73f2d7863911f9115317048c66a6ee/Libraries/Lists/SectionList.js)\ncomponents into `InvertedSectionList`.\n\nThe major change [here](https://github.com/LaunchPlatform/inverted-section-list/blob/69a44003500281d6b89166c59c407c5b9fa1050d/src/InvertedSectionList.tsx#L433-L438) is\nthat we are passing the footer indices as `stickyHeaderIndices` instead to the `VirtualizedList` since we are using\nfooter instead of header:\n\n```typescript\n// Notice: this is different from the original VirtualizedSectionList,\n//         since we are actually using footer as the header here, so need to + 1\n//         for the header\nstickyHeaderIndices.push(\n  itemCount + listHeaderOffset + sectionItemCount + 1\n);\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flaunchplatform%2Finverted-section-list","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flaunchplatform%2Finverted-section-list","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flaunchplatform%2Finverted-section-list/lists"}