react-nativeexporeact-native-flatlistreact-native-listview

Change flatlist ListHeaderComponentStyle on stick


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,
  },
});

screen recording


Solution

  • 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.