so im working on menu where there are categories and sub categories and the sub categories is a collapsible and the collapsible has smooth animation my issue right now im giving a fixed height for the parent of the collapsible
heightAnim.value = expanded ? data.length * 200 : 0;
to accommodate the items inside them ,how do I dynamically calculate it so that the height is given for each collapsible based on the items inside and if there any collapsible inside them the parent should accommodate based on toggle ,I'm a beginner to react I'm unable to figure it out any help and guidance will be useful
Here is my code below
import React, { useState, useEffect, useMemo } from "react";
import {
View,
Text,
TouchableOpacity,
StyleSheet,
ActivityIndicator,
} from "react-native";
import { FlashList } from "@shopify/flash-list";
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
} from "react-native-reanimated";
import { FlatList } from "react-native-gesture-handler";
// Function to fetch menu data
const fetchMenuData = async () => {
try {
const response = await fetch(
"https://piadnhuknkvucpovcwpf.supabase.co/storage/v1/object/public/project_assets//flat_menu.json"
);
return await response.json();
} catch (error) {
console.error("Error loading menu data:", error);
return [];
}
};
// CollapsibleSection functional component
const CollapsibleSection = ({
title,
data,
expanded: initialExpanded,
}: any) => {
const [expanded, setExpanded] = useState(initialExpanded);
const heightAnim = useSharedValue(0);
const opacityAnim = useSharedValue(0);
const animatedStyle = useAnimatedStyle(() => ({
height: withTiming(heightAnim.value, { duration: 300 }),
opacity: withTiming(opacityAnim.value, { duration: 300 }),
}));
useEffect(() => {
heightAnim.value = expanded ? data.length * 200 : 0; // Adjust based on item height
opacityAnim.value = expanded ? 1 : 0;
}, [expanded, data.length]);
const toggleExpand = () => {
setExpanded(!expanded);
};
return (
<View style={styles.section}>
<TouchableOpacity onPress={toggleExpand} style={styles.header}>
<Text style={styles.headerText}>{title}</Text>
</TouchableOpacity>
<Animated.View style={[styles.content, animatedStyle]}>
<FlatList
data={data}
keyExtractor={(item) => item.key}
scrollEnabled={false}
showsVerticalScrollIndicator={false}
renderItem={({ item }) => (
<View style={{ height: 50 }}>
<Text style={styles.item}>{item.name}</Text>
</View>
)}
/>
</Animated.View>
</View>
);
};
// Main App functional component
const MenuSectionList = () => {
interface MenuItem {
key: string;
name: string;
price: number;
mainCategoryHeader?: string;
subCategoryHeader?: string;
}
const [menuData, setMenuData] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const loadData = async () => {
const data = await fetchMenuData();
setMenuData(data);
setLoading(false);
};
loadData();
}, []);
const categoryMap = useMemo(() => {
const map: any = {};
menuData.forEach((item) => {
const mainCategory = item.mainCategoryHeader || "Other";
const subCategory = item.subCategoryHeader || "Miscellaneous";
if (!map[mainCategory]) {
map[mainCategory] = {};
}
if (!map[mainCategory][subCategory]) {
map[mainCategory][subCategory] = [];
}
map[mainCategory][subCategory].push(item);
});
return map;
}, [menuData]);
if (loading) {
return <ActivityIndicator size="large" color="#0000ff" />;
}
return (
<View style={styles.container}>
{/* Ensuring the FlashList parent container has a valid size */}
<View style={styles.flashListWrapper}>
<FlashList
data={Object.entries(categoryMap)}
keyExtractor={(item) => item[0]}
estimatedItemSize={150}
showsVerticalScrollIndicator={false}
renderItem={({ item }) => {
const [mainCategory, subCategories] = item;
return (
<View style={{ marginBottom: 20 }}>
<Text style={styles.mainCategory}>{mainCategory}</Text>
{Object.entries(subCategories).map(
([subCategory, items]) => (
<CollapsibleSection
key={subCategory}
title={subCategory}
data={items}
expanded={true} // Ensure each section is expanded on load
/>
)
)}
</View>
);
}}
/>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 10,
backgroundColor: "#f5f5f5",
},
section: {
backgroundColor: "#fff",
borderRadius: 8,
marginBottom: 10,
elevation: 3,
flexGrow: 1,
},
header: {
backgroundColor: "#007AFF",
padding: 15,
borderRadius: 8,
},
headerText: {
color: "white",
fontSize: 18,
fontWeight: "bold",
textAlign: "center",
},
content: {
overflow: "hidden",
},
item: {
padding: 10,
borderBottomWidth: 1,
borderBottomColor: "#ccc",
},
mainCategory: {
fontSize: 20,
fontWeight: "bold",
marginBottom: 5,
},
// Ensure the parent container for FlashList is valid and has a size
flashListWrapper: {
flex: 1,
},
});
export default MenuSectionList;
Here is a snack for the code expo snack
Please any help will appreciated
TL;DR
Link to the snack (which I've annotated with ADDED
and MODIFIED
tags to mark the changes I've made.)
https://snack.expo.dev/@j_sagar/solution
The solution:
Since you are initially keeping the dropdown/accordion open
when the parent mounts, we can get its height using the onLayout prop on it.
We can pass a callback function to onLayout
which returns an event object which has the height of the mounted View
as one of its nested properties.
When the component mounts, we can store this value in a ref variable as we'll only need to save this value once.
This way, instead of setting the height to a static value, you can dynamically animate the parent using its actual height.
The pitfall, of course, of this method is that this will work only when initially the dropdown/accordion is open
(which is what's in your current implementation). You can, however, tweak the code I've shared to make it work when it is not visible.