I am coding an accordion view with sub accordions and am searching through the accordions and their titles.
I want my accordions to open/close based on the below conditions:
Open/Close if I manually toggle the accordion
Open when the search condition is met
Closed if I am not searching ie searchQuery === ''
My bug is that if I click an accordion to open it, then start searching, the accordion which was clicked now becomes closed but all the other ones open.
import React, { useState, useEffect, useRef } from 'react';
import { SafeAreaView, Text, TouchableOpacity, View, ScrollView, Pressable } from 'react-native';
import { List, Searchbar } from 'react-native-paper';
import { useRouter } from 'expo-router'
import styles from './contents.styles.js'
import { COLORS, SIZES, icons, images, FONT } from '../../constants'
export default function Contents() {
const DATA = [
{
title: 'Section 1',
icon: 'folder',
indentation: 1,
color: COLORS.section1Blue.dark,
items: [
{
title: 'Section 1.1',
icon: 'folder',
indentation: 2,
color: COLORS.section1Blue.dark,
items: [
{
title: 'Section 1.1.1',
icon: 'file',
indentation: 3,
color: COLORS.section1Blue.light,
},
{
title: 'Section 1.1.2',
icon: 'file',
indentation: 3,
color: COLORS.section1Blue.light,
},
],
},
{
title: 'Section 1.2',
icon: 'file',
indentation: 2,
color: COLORS.section1Blue.light,
},
{
title: 'Section 1.3',
icon: 'file',
indentation: 2,
color: COLORS.section1Blue.light,
},
{
title: 'Section 1.4',
icon: 'file',
indentation: 2,
color: COLORS.section1Blue.light,
},
],
},
{
title: 'Section 2',
icon: 'folder',
indentation: 1,
color: COLORS.section1Blue.dark,
items: [
{
title: 'Section 2.1',
icon: 'folder',
indentation: 2,
color: COLORS.section1Blue.dark,
items: [
{
title: 'Section 2.1.1',
icon: 'file',
indentation: 3,
color: COLORS.section1Blue.light,
},
{
title: 'Section 2.1.2',
icon: 'file',
indentation: 3,
color: COLORS.section1Blue.light,
},
{
title: 'Section 2.1.3',
icon: 'file',
indentation: 3,
color: COLORS.section1Blue.light,
}
],
},
{
title: 'Section 2.2',
icon: 'file',
indentation: 2,
color: COLORS.section1Blue.light,
},
{
title: 'Section 2.3',
icon: 'file',
indentation: 2,
color: COLORS.section1Blue.light,
},
{
title: 'Section 2.4',
icon: 'file',
indentation: 2,
color: COLORS.section1Blue.light,
},
],
},
];
let [searchQuery, setSearchQuery] = useState('');
const router = useRouter()
const addIndentation = 30
let isOpen = useRef({});
const [isManuallyToggled, setIsManuallyToggled] = useState({});
const [isLoading, setIsLoading] = useState(false);
const handleAccordionToggle = (item) => {
setIsManuallyToggled({ ...isManuallyToggled, [item.title]: true });
isOpen.current[item.title] = !isOpen.current[item.title];
};
const filterByCondition = (array) => {
return array.reduce((accArray, obj) => {
if (obj.title.toLowerCase().includes(searchQuery.toLowerCase())) {
accArray.push({ ...obj }); // Include the object if the condition is met
} else if (obj.items) {
// Recursively filter items array if it exists and is not empty
const items = filterByCondition(obj.items);
if (items.length > 0) {
// Include the object if it has non-empty items array after filtering
accArray.push({ ...obj, items });
}
}
return accArray; // Return the accumulated array
}, []);
};
// Filter the array based on the condition
const filteredArray = filterByCondition(DATA); // Create a copy of DATA using spread operator
useEffect(() => {
setIsLoading(true);
setTimeout(() => {
setIsLoading(false);
}, 0);
}, [searchQuery]);
const anyItemMatchesQuery = (items, query) => {
return items.some(item =>
item.title.toLowerCase().includes(query.toLowerCase()) ||
(item.items && anyItemMatchesQuery(item.items, query))
);
};
const renderItems = (items) =>
items.map((item, index) =>
item.icon === 'file' ? (
<TouchableOpacity
style={{ paddingLeft: addIndentation * item.indentation }}
key={`main-view-${item.title}`}
onPress={() =>
router.push({
params: {
id: item.id,
title: item.title,
imageTitle: item.images.map(image => image.title),
imageDescription: item.images.map(image => image.description),
imageCredit: item.images.map(image => image.credit),
imageLink: item.images.map(image => image.link),
commonDescription: item.commonDescription,
},
pathname: `/main-view/${item.title}`,
})
}
>
<List.Item
titleStyle={{ fontFamily: FONT.regular }}
left={() => <List.Icon icon={item.icon} color={item.color} />}
key={index}
title={item.title}
/>
</TouchableOpacity>
) : (
<List.Accordion
titleStyle={{ fontFamily: FONT.regular }}
theme={{ colors: { onSurfaceVariant: item.color, primary: item.color } }}
onPress={() => {
handleAccordionToggle(item);
setIsManuallyToggled({ ...isManuallyToggled, [item.title]: true });
}}
expanded={(
isManuallyToggled[item.title]) && searchQuery.length !== 0
? !isOpen.current[item.title]
: isManuallyToggled[item.title] && searchQuery === ''
? isOpen.current[item.title]
: (searchQuery !== '' && anyItemMatchesQuery(item.items, searchQuery))
}
key={item.title}
title={item.title}
left={() => <List.Icon icon={item.icon} color={item.color} />}
style={{ paddingLeft: addIndentation * item.indentation }}
>
{renderItems(item.items)}
</List.Accordion>
),
);
return (
<View style={{ backgroundColor: 'white' }}>
<Searchbar
right={query => (
<TouchableOpacity activeOpacity={0.5} onPress={() => setSearchQuery('')}>
<List.Icon style={styles.clearIcon} icon="close-circle-outline" />
</TouchableOpacity>)}
placeholder="Search"
onChangeText={query => setSearchQuery(query)}
value={searchQuery}
loading={isLoading}
inputStyle={{ fontFamily: FONT.regular }}
/>
<List.Section>
{renderItems(filteredArray)}
</List.Section>
</View>
);
};
I have tried many different solutions and each time I find a different solution which comes up with another bug. I think one of the problems may have been around setState and the value not updating when I want it to but I am not a react expert so I may be wrong
Focus on: setIsExpended({ id: -1 });
Expo Snack Link here