From f516080e6e9735ca993866d5c7aa4c0b5fc52e90 Mon Sep 17 00:00:00 2001 From: Mateusz Wit Date: Wed, 10 Jul 2024 20:06:04 +0200 Subject: [PATCH] fix(#814): update sticky headers when data changes without changing stickyHeaderIndices updated --- CHANGELOG.md | 3 + fixture/react-native/src/ExamplesScreen.tsx | 1 + fixture/react-native/src/NavigationTree.tsx | 2 + fixture/react-native/src/SectionList.tsx | 189 ++++++++++++++++++++ fixture/react-native/src/constants.ts | 1 + src/FlashList.tsx | 4 +- 6 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 fixture/react-native/src/SectionList.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ead4ed31..43a541d3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## [Unreleased] +- Fix first sticky header is not rendering when data changed + - https://github.com/Shopify/flash-list/issues/814 + ## [1.7.0] - 2024-07-03 - Update internal dependency and fixture app to `react-native@0.72`. diff --git a/fixture/react-native/src/ExamplesScreen.tsx b/fixture/react-native/src/ExamplesScreen.tsx index 1e9f04144..ed473ef77 100644 --- a/fixture/react-native/src/ExamplesScreen.tsx +++ b/fixture/react-native/src/ExamplesScreen.tsx @@ -21,6 +21,7 @@ export const ExamplesScreen = () => { const data: ExampleItem[] = [ { title: "List", destination: "List" }, + { title: "SectionList", destination: "SectionList" }, { title: "PaginatedList", destination: "PaginatedList" }, { title: "Reminders", destination: "Reminders" }, { title: "Twitter Timeline", destination: "Twitter" }, diff --git a/fixture/react-native/src/NavigationTree.tsx b/fixture/react-native/src/NavigationTree.tsx index 5288683f1..22789d032 100644 --- a/fixture/react-native/src/NavigationTree.tsx +++ b/fixture/react-native/src/NavigationTree.tsx @@ -15,6 +15,7 @@ import { Messages, MessagesFlatList } from "./Messages"; import TwitterBenchmark from "./twitter/TwitterBenchmark"; import TwitterCustomCellContainer from "./twitter/CustomCellRendererComponent"; import { Masonry } from "./Masonry"; +import { SectionList } from "./SectionList"; const Stack = createStackNavigator(); @@ -25,6 +26,7 @@ const NavigationTree = () => { + diff --git a/fixture/react-native/src/SectionList.tsx b/fixture/react-native/src/SectionList.tsx new file mode 100644 index 000000000..e7187550d --- /dev/null +++ b/fixture/react-native/src/SectionList.tsx @@ -0,0 +1,189 @@ +/** * + Use this component inside your React Native Application. + A scrollable list with different item type + */ +import React, { useMemo, useRef, useState } from "react"; +import { + View, + Text, + Pressable, + LayoutAnimation, + StyleSheet, +} from "react-native"; +import { FlashList, ListRenderItemInfo } from "@shopify/flash-list"; + +const generateItemsArray = (size: number) => { + const arr = new Array(size); + for (let i = 0; i < size; i++) { + arr[i] = i; + } + return arr; +}; + +const generateSectionsData = (size: number, index = 0) => { + const arr = new Array<{ index: number; data: number[] }>(size); + for (let i = 0; i < size; i++) { + arr[i] = { + index: index + i, + data: generateItemsArray(10), + }; + } + return arr; +}; + +interface Section { + type: "section"; + index: number; +} + +interface Item { + type: "item"; + index: number; + sectionIndex: number; +} + +type SectionListItem = Section | Item; + +export const SectionList = () => { + const [refreshing, setRefreshing] = useState(false); + const [sections, setSections] = useState(generateSectionsData(10)); + + const list = useRef | null>(null); + + const flattenedSections = useMemo( + () => + sections.reduce((acc, { index, data }) => { + const items: Item[] = data.map((itemIndex) => ({ + type: "item", + index: itemIndex, + sectionIndex: index, + })); + + return [...acc, { index, type: "section" }, ...items]; + }, []), + [sections] + ); + + const stickyHeaderIndices = useMemo( + () => + flattenedSections + .map((item, index) => (item.type === "section" ? index : undefined)) + .filter((item) => item !== undefined) as number[], + [flattenedSections] + ); + + const removeItem = (item: Item) => { + setSections( + sections.map((section) => ({ + ...section, + data: section.data.filter((index) => index === item.index), + })) + ); + list.current?.prepareForLayoutAnimationRender(); + // after removing the item, we start animation + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + }; + + const removeSection = () => { + setSections(sections.slice(1)); + list.current?.prepareForLayoutAnimationRender(); + // after removing the item, we start animation + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + }; + + const addSection = () => { + const lastIndex = sections[sections.length - 1].index; + setSections([...sections, ...generateSectionsData(1, lastIndex + 1)]); + list.current?.prepareForLayoutAnimationRender(); + // after removing the item, we start animation + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + }; + + const renderItem = ({ item }: ListRenderItemInfo) => { + if (item.type === "section") { + return ( + + Section: {item.index} + + ); + } + + const backgroundColor = item.index % 2 === 0 ? "#00a1f1" : "#ffbb00"; + + return ( + removeItem(item)}> + 97 ? "red" : backgroundColor, + height: item.index % 2 === 0 ? 100 : 200, + }} + > + + Section: {item.sectionIndex} Cell Id: {item.index} + + + + ); + }; + + return ( + <> + + + + Add Section + + + + + Remove Section + + + + + { + setRefreshing(true); + setTimeout(() => { + setRefreshing(false); + }, 2000); + }} + keyExtractor={(item) => + item.type === "section" + ? `${item.index}` + : `${item.sectionIndex}${item.index}` + } + renderItem={renderItem} + estimatedItemSize={100} + stickyHeaderIndices={stickyHeaderIndices} + data={flattenedSections} + /> + + ); +}; + +const styles = StyleSheet.create({ + container: { + justifyContent: "space-around", + alignItems: "center", + height: 120, + backgroundColor: "#00a1f1", + }, + buttonsContainer: { + flexDirection: "row", + justifyContent: "space-around", + }, + button: { + padding: 10, + }, +}); diff --git a/fixture/react-native/src/constants.ts b/fixture/react-native/src/constants.ts index d27d7b700..7a2b82627 100644 --- a/fixture/react-native/src/constants.ts +++ b/fixture/react-native/src/constants.ts @@ -5,6 +5,7 @@ import { TweetContentProps } from "./twitter/TweetContent"; export type RootStackParamList = { Examples: undefined; List: undefined; + SectionList: undefined; Reminders: undefined; PaginatedList: undefined; Twitter: undefined; diff --git a/src/FlashList.tsx b/src/FlashList.tsx index fa851f0db..bf08642fb 100644 --- a/src/FlashList.tsx +++ b/src/FlashList.tsx @@ -693,7 +693,7 @@ class FlashList extends React.PureComponent< private stickyOverrideRowRenderer = ( _: any, - __: any, + rowData: any, index: number, ___: any ) => { @@ -701,6 +701,8 @@ class FlashList extends React.PureComponent<