OiO.lk Community platform!

Oio.lk is an excellent forum for developers, providing a wide range of resources, discussions, and support for those in the developer community. Join oio.lk today to connect with like-minded professionals, share insights, and stay updated on the latest trends and technologies in the development field.
  You need to log in or register to access the solved answers to this problem.
  • You have reached the maximum number of guest views allowed
  • Please register below to remove this limitation

How Can I Prevent Re-Render In Nested Flatlist When Changing State?

  • Thread starter Thread starter africanxmamba
  • Start date Start date
A

africanxmamba

Guest
For the past two weeks, I've been encountering this problem with my nested Flatlist (horizontal flatlist inside of a vertical flatlist). For those who are familiar with Instagram, I'm trying to create a view that contains a feed along with a set of options to view. You can see that I've included a list footer component and list header component. I am attempting to change something in the header component.

When I select an option, it should change the background of the option to indicate that it's been selected and the feed should update accordingly. However, every single time I select an option, the entire screen re-renders. When I select the option, the carousel restarts at the first option, the horizontal flatlist goes back to the beginning and the screen flickers. This should not be the case at all because it is make the user experience unexpected. I've seen other applications do it before that also use React Native, but I'm unsure why this has been the case. I've attached a video for everyone to see along with my code.

Code:
export default function Home() {
  const { authState } = useAuth();
  const [userFeed, setUserFeed] = useState<Feed | null>(null);
  const [loading, setLoading] = useState<boolean>(true);
  const [refreshing, setRefreshing] = useState<boolean>(false);
  const [selectedOption, setSelectedOption] = useState<string>("2");
  const [articles, setArticles] = useState<any[] | null>([]);

  const PAGE_WIDTH = window.width;

  const colorScheme = useColorScheme();
  const dotStyleColor = colorScheme === 'dark' ? "grey" : "rgba(0,0,0,0.2)";

  const ref = useRef<ICarouselInstance>(null);

  // Render the footer when the end of the feed is reached
  const renderFooter = () => {
    return (
      <View style={styles.footer}>
        {/* <ActivityIndicator size="large" /> */}
      </View>
    );
  };

  const renderHeader = () => {
    return (
      <React.Fragment>
        <ThemedText style={styles.title}>Headlines</ThemedText>
        <ThemedView style={styles.carousel}>
          <Carousel
            ref={ref}
            {...baseOptions}
            style={{ width: PAGE_WIDTH }}
            loop
            pagingEnabled={true}
            snapEnabled={true}
            autoPlay={true}
            autoPlayInterval={3600}
            mode="parallax"
            modeConfig={{
              parallaxScrollingScale: 0.9,
              parallaxScrollingOffset: 50,
            }}
            data={userFeed.headlines}
            onProgressChange={(_, absoluteProgress) => {
              progress.value = absoluteProgress;
            }}
            renderItem={({ index, item }) => <Headliner index={index} headline={item} />}
          />
          <Pagination.Basic<{ index: number }>
            progress={progress}
            data={combinedData.map((_, index) => ({ index }))}
            dotStyle={{
              width: 25,
              height: 4,
              backgroundColor: dotStyleColor,
            }}
            activeDotStyle={{
              overflow: "hidden",
            }}
            containerStyle={[
              undefined,
              {
                gap: 10,
                marginBottom: 10,
              },
            ]}
            horizontal={true}
            onPress={onPressPagination}
          />
        </ThemedView>

        <ThemedView id="feed-options" style={styles.optionsContainer}>
          <FlatList
            data={options}
            renderItem={renderItem}
            keyExtractor={(item) => item.id}
            horizontal={true}
            showsHorizontalScrollIndicator={false}
            contentContainerStyle={styles.flatListContent}
            extraData={selectedOption}  // Ensure FlatList re-renders only affected items
          />
        </ThemedView>

        <ThemedView style={{ marginLeft: 20, marginBottom: 5 }}>
          <ThemedText style={{ fontWeight: "bold", fontSize: 25 }}>
            {options.find((option) => option.id === selectedOption)?.title ?? ""}
          </ThemedText>
        </ThemedView>
      </React.Fragment>
    )
  };


  const handlePress = useCallback((id: string) => {
    setSelectedOption(id);
  }, []);

  const renderItem = useCallback(({ item }) => {
    return (
      <FlatlistOption
        id={item.id}
        title={item.title}
        isSelected={item.id === selectedOption}
        onPress={handlePress}
      />
    );
  }, [selectedOption, handlePress]);

  useEffect(() => {
    // Fetch the user's feed if they are authenticated
    if (authState?.authenticated === true) {

      // Function to fetch the user's feed
      const fetchFeed = async (): Promise<any> => {
        try {
          const response = await axios.get(`/users/${authState?.id}/feed`);
          return response.data;
        } catch (error) {
          console.error('Error fetching user feed:', error);
          return null;
        }
      };

      // Function to get the user's feed and set the state
      const getUserFeed = async () => {
        const feed = await fetchFeed();
        setUserFeed(feed);
      };

      // Fetch the user's feed
      getUserFeed();
      setLoading(false);
    }
  }, [authState?.authenticated, authState?.id]);

  // In the future this should be dynamic based on the selected option
  useEffect(() => {
    switch (selectedOption) {
      case '2':
        setArticles(articles_for_you);
        break;
      case '3':
        setArticles(articles_women_in_hip_hop);
        break;
      case '10':
        setArticles(articles_business);
        break;
      // Add more cases for other options
      default:
        setArticles([]);
        break;
    }
  }, [selectedOption]);

  const progress = useSharedValue<number>(0);
  const baseOptions = ({
    vertical: false,
    width: PAGE_WIDTH,
    height: PAGE_WIDTH * 0.6,
  } as const);

  // Function to simulate fetching the user's feed with a 10-second delay
  // TODO: Remove this function and replace it with the actual API call
  const mockGetUserFeed = async (): Promise<void> => {
    return new Promise((resolve) => {
      setTimeout(async () => {
        console.log("Mock fetching user feed...")
        resolve();
      }, 3000);
    });
  };

  // Function to refresh the user's feed
  const onRefresh = async () => {
    setRefreshing(true);
    // Fetch the user feed and stop the refreshing indicator when done
    await mockGetUserFeed();
    setRefreshing(false);
  };

  // Store index of the carousel when the user swipes
  const onPressPagination = (index: number) => {
    ref.current?.scrollTo({
      index: index,
      animated: true,
    });
  };

  const onEndReached = () => {
    // console.log("End reached");
  };

  // If the feed is still loading, show a loading indicator
  if (loading || !userFeed) {
    return (
      <ThemedView style={styles.container}>
        <ActivityIndicator size="large" />
      </ThemedView>
    );
  }

  // Combine the Poll and headlines into a single array
  const combinedData = [userFeed.poll, ...userFeed.headlines];

  return (
    <ThemedView style={styles.container}>
      <FlatList
        data={articles}
        renderItem={({ item }) => <HomePageArticle article={item} />}
        keyExtractor={(item) => item.id}
        showsVerticalScrollIndicator={false}
        onEndReached={onEndReached}
        onEndReachedThreshold={0.5}
        ListFooterComponent={renderFooter}
        refreshControl={
          <RefreshControl
            refreshing={refreshing}
            onRefresh={onRefresh}
          />
        }
        ListHeaderComponent={renderHeader}
      />
    </ThemedView>
  );
}

https://gifyu.com/image/SrwgS

I've tried almost by use React.memo for the renderItem along with useCallback for setting the selectedOptionId, but neither option has worked. If anyone could please, help me with this issue it would be truly appreciated.

<p>For the past two weeks, I've been encountering this problem with my nested Flatlist (horizontal flatlist inside of a vertical flatlist). For those who are familiar with Instagram, I'm trying to create a view that contains a feed along with a set of options to view. You can see that I've included a list footer component and list header component. I am attempting to change something in the header component.</p>
<p>When I select an option, it should change the background of the option to indicate that it's been selected and the feed should update accordingly. However, every single time I select an option, the entire screen re-renders. When I select the option, the carousel restarts at the first option, the horizontal flatlist goes back to the beginning and the screen flickers. This should not be the case at all because it is make the user experience unexpected. I've seen other applications do it before that also use React Native, but I'm unsure why this has been the case. I've attached a video for everyone to see along with my code.</p>
<pre><code>export default function Home() {
const { authState } = useAuth();
const [userFeed, setUserFeed] = useState<Feed | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [refreshing, setRefreshing] = useState<boolean>(false);
const [selectedOption, setSelectedOption] = useState<string>("2");
const [articles, setArticles] = useState<any[] | null>([]);

const PAGE_WIDTH = window.width;

const colorScheme = useColorScheme();
const dotStyleColor = colorScheme === 'dark' ? "grey" : "rgba(0,0,0,0.2)";

const ref = useRef<ICarouselInstance>(null);

// Render the footer when the end of the feed is reached
const renderFooter = () => {
return (
<View style={styles.footer}>
{/* <ActivityIndicator size="large" /> */}
</View>
);
};

const renderHeader = () => {
return (
<React.Fragment>
<ThemedText style={styles.title}>Headlines</ThemedText>
<ThemedView style={styles.carousel}>
<Carousel
ref={ref}
{...baseOptions}
style={{ width: PAGE_WIDTH }}
loop
pagingEnabled={true}
snapEnabled={true}
autoPlay={true}
autoPlayInterval={3600}
mode="parallax"
modeConfig={{
parallaxScrollingScale: 0.9,
parallaxScrollingOffset: 50,
}}
data={userFeed.headlines}
onProgressChange={(_, absoluteProgress) => {
progress.value = absoluteProgress;
}}
renderItem={({ index, item }) => <Headliner index={index} headline={item} />}
/>
<Pagination.Basic<{ index: number }>
progress={progress}
data={combinedData.map((_, index) => ({ index }))}
dotStyle={{
width: 25,
height: 4,
backgroundColor: dotStyleColor,
}}
activeDotStyle={{
overflow: "hidden",
}}
containerStyle={[
undefined,
{
gap: 10,
marginBottom: 10,
},
]}
horizontal={true}
onPress={onPressPagination}
/>
</ThemedView>

<ThemedView id="feed-options" style={styles.optionsContainer}>
<FlatList
data={options}
renderItem={renderItem}
keyExtractor={(item) => item.id}
horizontal={true}
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.flatListContent}
extraData={selectedOption} // Ensure FlatList re-renders only affected items
/>
</ThemedView>

<ThemedView style={{ marginLeft: 20, marginBottom: 5 }}>
<ThemedText style={{ fontWeight: "bold", fontSize: 25 }}>
{options.find((option) => option.id === selectedOption)?.title ?? ""}
</ThemedText>
</ThemedView>
</React.Fragment>
)
};


const handlePress = useCallback((id: string) => {
setSelectedOption(id);
}, []);

const renderItem = useCallback(({ item }) => {
return (
<FlatlistOption
id={item.id}
title={item.title}
isSelected={item.id === selectedOption}
onPress={handlePress}
/>
);
}, [selectedOption, handlePress]);

useEffect(() => {
// Fetch the user's feed if they are authenticated
if (authState?.authenticated === true) {

// Function to fetch the user's feed
const fetchFeed = async (): Promise<any> => {
try {
const response = await axios.get(`/users/${authState?.id}/feed`);
return response.data;
} catch (error) {
console.error('Error fetching user feed:', error);
return null;
}
};

// Function to get the user's feed and set the state
const getUserFeed = async () => {
const feed = await fetchFeed();
setUserFeed(feed);
};

// Fetch the user's feed
getUserFeed();
setLoading(false);
}
}, [authState?.authenticated, authState?.id]);

// In the future this should be dynamic based on the selected option
useEffect(() => {
switch (selectedOption) {
case '2':
setArticles(articles_for_you);
break;
case '3':
setArticles(articles_women_in_hip_hop);
break;
case '10':
setArticles(articles_business);
break;
// Add more cases for other options
default:
setArticles([]);
break;
}
}, [selectedOption]);

const progress = useSharedValue<number>(0);
const baseOptions = ({
vertical: false,
width: PAGE_WIDTH,
height: PAGE_WIDTH * 0.6,
} as const);

// Function to simulate fetching the user's feed with a 10-second delay
// TODO: Remove this function and replace it with the actual API call
const mockGetUserFeed = async (): Promise<void> => {
return new Promise((resolve) => {
setTimeout(async () => {
console.log("Mock fetching user feed...")
resolve();
}, 3000);
});
};

// Function to refresh the user's feed
const onRefresh = async () => {
setRefreshing(true);
// Fetch the user feed and stop the refreshing indicator when done
await mockGetUserFeed();
setRefreshing(false);
};

// Store index of the carousel when the user swipes
const onPressPagination = (index: number) => {
ref.current?.scrollTo({
index: index,
animated: true,
});
};

const onEndReached = () => {
// console.log("End reached");
};

// If the feed is still loading, show a loading indicator
if (loading || !userFeed) {
return (
<ThemedView style={styles.container}>
<ActivityIndicator size="large" />
</ThemedView>
);
}

// Combine the Poll and headlines into a single array
const combinedData = [userFeed.poll, ...userFeed.headlines];

return (
<ThemedView style={styles.container}>
<FlatList
data={articles}
renderItem={({ item }) => <HomePageArticle article={item} />}
keyExtractor={(item) => item.id}
showsVerticalScrollIndicator={false}
onEndReached={onEndReached}
onEndReachedThreshold={0.5}
ListFooterComponent={renderFooter}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
/>
}
ListHeaderComponent={renderHeader}
/>
</ThemedView>
);
}
</code></pre>
<p><a href="https://gifyu.com/image/SrwgS" rel="nofollow noreferrer">https://gifyu.com/image/SrwgS</a></p>
<p>I've tried almost by use React.memo for the renderItem along with useCallback for setting the selectedOptionId, but neither option has worked. If anyone could please, help me with this issue it would be truly appreciated.</p>
 

Latest posts

Online statistics

Members online
0
Guests online
5
Total visitors
5
Top