You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Native Paper List Accordion expanded属性异常问题排查

修复带搜索功能的嵌套Accordion面板状态异常问题

问题根源

你的核心问题出在状态管理混乱和useRef的错误使用:

  • 用useRef存储isOpen,但useRef的变更不会触发组件重渲染,导致UI和实际状态不同步
  • expanded属性的判断逻辑过于复杂,手动切换和搜索状态的优先级混乱,引发搜索时错误的展开/收起行为
  • 手动切换时同时更新isManuallyToggled和isOpen.current,两者更新时机不一致,造成状态冲突

解决方案

把isOpen从useRef改成useState统一管理状态,同时简化expanded的判断规则,明确优先级:

  1. 手动操作过的面板,优先遵循手动设置的展开状态
  2. 未手动操作过的面板,搜索时自动展开匹配项
  3. 清空搜索时,所有面板自动收起

修改后的完整代码

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

关键改动说明

  1. 替换useRef为useState:确保状态变更能触发组件重渲染,UI与状态保持同步
  2. 函数式状态更新:避免直接依赖状态快照,确保更新逻辑的准确性
  3. 简化expanded判断:优先处理手动操作的面板,未手动操作的面板根据搜索结果自动展开
  4. 清空搜索重置状态:确保清空搜索后所有面板自动收起,符合需求

内容的提问来源于stack exchange,提问作者James

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 12:34:57