Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(#814): update sticky headers when data changes without changing s… #1267

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

- Add recomputeViewableItems method
- https://github.com/Shopify/flash-list/pull/1296
- Fix first sticky header is not rendering when data changed
- https://github.com/Shopify/flash-list/issues/814

## [1.7.0] - 2024-07-03

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
Loading