React Native Paper List Accordion expanded属性异常问题排查
修复带搜索功能的嵌套Accordion面板状态异常问题
问题根源
你的核心问题出在状态管理混乱和useRef的错误使用:
- 用
useRef存储isOpen,但useRef的变更不会触发组件重渲染,导致UI和实际状态不同步 expanded属性的判断逻辑过于复杂,手动切换和搜索状态的优先级混乱,引发搜索时错误的展开/收起行为- 手动切换时同时更新
isManuallyToggled和isOpen.current,两者更新时机不一致,造成状态冲突
解决方案
把isOpen从useRef改成useState统一管理状态,同时简化expanded的判断规则,明确优先级:
- 手动操作过的面板,优先遵循手动设置的展开状态
- 未手动操作过的面板,搜索时自动展开匹配项
- 清空搜索时,所有面板自动收起
修改后的完整代码
import React, { useState, useEffect } from 'react'; import { TouchableOpacity, View } from 'react-native'; import { List, Searchbar } from 'react-native-paper'; import { useRouter } from 'expo-router' import styles from './contents.styles.js' import { COLORS, 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, }, ], }, ]; const [searchQuery, setSearchQuery] = useState(''); const router = useRouter() const addIndentation = 30; // 关键改动1:用useState替代useRef存储展开状态,确保状态变更触发重渲染 const [isOpen, setIsOpen] = useState({}); const [isManuallyToggled, setIsManuallyToggled] = useState({}); const [isLoading, setIsLoading] = useState(false); const handleAccordionToggle = (item) => { // 关键改动2:使用函数式更新状态,避免快照问题 setIsOpen(prev => ({ ...prev, [item.title]: !prev[item.title] })); setIsManuallyToggled(prev => ({ ...prev, [item.title]: true })); }; const filterByCondition = (array) => { return array.reduce((accArray, obj) => { if (obj.title.toLowerCase().includes(searchQuery.toLowerCase())) { accArray.push({ ...obj }); } else if (obj.items) { const items = filterByCondition(obj.items); if (items.length > 0) { accArray.push({ ...obj, items }); } } return accArray; }, []); }; const filteredArray = filterByCondition(DATA); useEffect(() => { setIsLoading(true); setTimeout(() => { setIsLoading(false); }, 0); // 关键改动3:清空搜索时重置所有状态,回到全部收起 if (!searchQuery) { setIsManuallyToggled({}); setIsOpen({}); } }, [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) => 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} />} title={item.title} /> </TouchableOpacity> ) : ( <List.Accordion titleStyle={{ fontFamily: FONT.regular }} theme={{ colors: { onSurfaceVariant: item.color, primary: item.color } }} onPress={() => handleAccordionToggle(item)} // 关键改动4:简化expanded判断逻辑,明确优先级 expanded={ // 手动操作过的面板,遵循手动设置的状态 isManuallyToggled[item.title] ? isOpen[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={() => ( <TouchableOpacity activeOpacity={0.5} onPress={() => setSearchQuery('')}> <List.Icon style={styles.clearIcon} icon="close-circle-outline" /> </TouchableOpacity>)} placeholder="Search" onChangeText={setSearchQuery} value={searchQuery} loading={isLoading} inputStyle={{ fontFamily: FONT.regular }} /> <List.Section> {renderItems(filteredArray)} </List.Section> </View> ); };
关键改动说明
- 替换useRef为useState:确保状态变更能触发组件重渲染,UI与状态保持同步
- 函数式状态更新:避免直接依赖状态快照,确保更新逻辑的准确性
- 简化expanded判断:优先处理手动操作的面板,未手动操作的面板根据搜索结果自动展开
- 清空搜索重置状态:确保清空搜索后所有面板自动收起,符合需求
内容的提问来源于stack exchange,提问作者James
相关产品推荐
相关产品推荐

