I'm making a simple react-native shopping list App. I'm rendering everything in a FlatList
as shown in the code. I want to change the ListHeaderComponentStyle
whenever the header component sticks.
My try:
After doing a Google search, I asked GPT to solve this problem it recommends checking this using onViewableItemsChanged
This approach is working but not properly syncing with the scroll which means I can't play animation and also it is flickering.
import { FlatList, StyleSheet, TextInput, View } from "react-native";
import { theme } from "../theme";
import ShoppingListItem from "../components/ShoppingListItem";
import { useCallback, useRef, useState } from "react";
import { shoppingListItems } from "../lib/constants";
export default function App() {
const [shoppingList, setShoppingList] = useState(
new Array(300)
.fill(shoppingListItems)
.flat()
.map((val, idx) => ({ ...val, id: idx })),
);
const [input, setInput] = useState<string>("");
const handleSubmit = useCallback(() => {
if (input.trim().length === 0) return;
setShoppingList((prevList) => [
{
name: input.trim(),
id: +new Date(),
},
...prevList,
]);
setInput("");
}, [input]);
const [isHeaderSticky, setIsHeaderSticky] = useState(false);
// const viewabilityConfig = useRef({
// itemVisiblePercentThreshold: 50,
// });
// const onViewableItemsChanged = useCallback(({ viewableItems }) => {
// const isFirstItemVisible = viewableItems.some(
// (item) => item.index === 0
// );
// setIsHeaderSticky(!isFirstItemVisible);
// }, []);
const viewabilityConfig = useRef({
itemVisiblePercentThreshold: 50,
});
return (
<>
<FlatList
data={shoppingList}
stickyHeaderIndices={[0]}
// stickyHeaderHiddenOnScroll
style={styles.container}
contentContainerStyle={styles.contentContainer}
viewabilityConfig={viewabilityConfig.current}
onViewableItemsChanged={(info) => {
const isFirstItemVisible = info.viewableItems.some(
(item) => item.index === 0,
);
setIsHeaderSticky(!isFirstItemVisible);
}}
ListHeaderComponentStyle={
isHeaderSticky
? {
// TODO: only apply border when it stick
borderBottomWidth: 1,
borderBottomColor: theme.colorCerulean,
backgroundColor: theme.colorWhite,
}
: undefined
}
ListHeaderComponent={
<View style={styles.inputWrapper}>
<TextInput
placeholder="e.g Coffee"
style={styles.input}
value={input}
onChangeText={setInput}
// keyboardType=""
// returnKeyType="done"
onSubmitEditing={handleSubmit}
/>
</View>
}
renderItem={({ item }) => {
return <ShoppingListItem name={item.name} />;
}}
/>
</>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: theme.colorWhite,
},
contentContainer: {
paddingTop: 24,
},
inputWrapper: {
paddingHorizontal: 8,
marginBottom: 24,
backgroundColor: theme.colorWhite,
paddingVertical: 4,
// borderBottomWidth: 1,
// borderBottomColor: theme.colorLightGrey,
},
input: {
borderWidth: 1,
borderColor: theme.colorLightGrey,
borderRadius: 8,
padding: 12,
},
});
After posting this post at several places one Expo discord member @KingSlayer suggested I use the onScroll
callback.
The flickering issue occurs because onViewableItemsChanged is not in sync with the scrolling updates, which makes it difficult to manage animations or smooth transitions for your sticky header. Try using onScroll combined with Animated to detect when the header becomes sticky and then smoothly update.
Here is my updated code properly syncs with the scroll.
<FlatList
onScroll={(e) => {
setIsHeaderSticky(e.nativeEvent.contentOffset.y > 19);
}}
/>
Important note! In your case threshold 19
may not work. so make sure to find the best value for your scenario by logging value first.