Skip to content

Commit

Permalink
fix(Shopify#814): update sticky headers when data changes without cha…
Browse files Browse the repository at this point in the history
…nging stickyHeaderIndices updated
  • Loading branch information
MateWW committed Jul 10, 2024
1 parent 16226b2 commit 7c5f69b
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 1 deletion.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
1 change: 1 addition & 0 deletions fixture/react-native/src/ExamplesScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down
2 changes: 2 additions & 0 deletions fixture/react-native/src/NavigationTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<RootStackParamList>();

Expand All @@ -25,6 +26,7 @@ const NavigationTree = () => {
<Stack.Group>
<Stack.Screen name="Examples" component={ExamplesScreen} />
<Stack.Screen name="List" component={List} />
<Stack.Screen name="SectionList" component={SectionList} />
<Stack.Screen name="PaginatedList" component={PaginatedList} />
<Stack.Screen name="Twitter" component={Twitter} />
<Stack.Screen name="Reminders" component={Reminders} />
Expand Down
189 changes: 189 additions & 0 deletions fixture/react-native/src/SectionList.tsx
Original file line number Diff line number Diff line change
@@ -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<FlashList<SectionListItem> | null>(null);

const flattenedSections = useMemo(
() =>
sections.reduce<SectionListItem[]>((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<SectionListItem>) => {
if (item.type === "section") {
return (
<View
style={{
...styles.container,
backgroundColor: "purple",
width: "100%",
height: 30,
}}
>
<Text>Section: {item.index}</Text>
</View>
);
}

const backgroundColor = item.index % 2 === 0 ? "#00a1f1" : "#ffbb00";

return (
<Pressable onPress={() => removeItem(item)}>
<View
style={{
...styles.container,
backgroundColor: item.index > 97 ? "red" : backgroundColor,
height: item.index % 2 === 0 ? 100 : 200,
}}
>
<Text>
Section: {item.sectionIndex} Cell Id: {item.index}
</Text>
</View>
</Pressable>
);
};

return (
<>
<View style={styles.buttonsContainer}>
<Pressable onPress={addSection}>
<View style={styles.button}>
<Text>Add Section</Text>
</View>
</Pressable>
<Pressable onPress={removeSection}>
<View style={styles.button}>
<Text>Remove Section</Text>
</View>
</Pressable>
</View>

<FlashList
ref={list}
refreshing={refreshing}
onRefresh={() => {
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,
},
});
1 change: 1 addition & 0 deletions fixture/react-native/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { TweetContentProps } from "./twitter/TweetContent";
export type RootStackParamList = {
Examples: undefined;
List: undefined;
SectionList: undefined;
Reminders: undefined;
PaginatedList: undefined;
Twitter: undefined;
Expand Down
4 changes: 3 additions & 1 deletion src/FlashList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -693,14 +693,16 @@ class FlashList<T> extends React.PureComponent<

private stickyOverrideRowRenderer = (
_: any,
__: any,
rowData: any,
index: number,
___: any
) => {
return (
<PureComponentWrapper
ref={this.stickyContentRef}
enabled={this.isStickyEnabled}
// We're passing rowData to ensure that sticky headers are updated when data changes
rowData={rowData}
arg={index}
renderer={this.rowRendererSticky}
/>
Expand Down

0 comments on commit 7c5f69b

Please sign in to comment.