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

React.js中如何实现多级动态Tabs标签页的状态持久化?

问题根因

你当前的子标签选中状态仅存储在IndividualSelectingAttributeContainer组件的局部useState中,未和对应主标签、属性的唯一标识绑定:

  • 切换主标签时React会复用同类型组件实例,导致旧状态被带到新渲染的主标签下,出现状态串扰
  • 切回之前操作过的主标签时,局部状态会重置为初始值0,无法保留之前的选中记录
修改方案

我们需要在父层新增全局状态存储每个属性的选中值,用主标签ID、属性ID作为唯一key绑定状态,同时给所有列表渲染项添加唯一key避免组件复用异常。

核心修改点

  1. 在ProductTypesTable组件中新增选中状态存储,结构为{ [主标签ID]: { [属性ID]: 选中索引 } }
  2. 移除子组件IndividualSelectingAttributeContainer的局部选中状态,改用父层传递的状态和更新方法
  3. 所有map渲染的列表项添加唯一key属性

完整修改后代码

import "./styles.css";
import React, { useState } from "react";
const productTypesSizeAttributesList = [
  [
    {
      product_type_id: "1",
      attribute_id: "1",
      attribute_name: "Achara Cloth",
      product_type_method_id: 3,
      product_type_type_id: 3,
      product_type_unit_id: 4,
      attribute_gif_url: "https://i.imgur.com/cr0LVC7.jpg",
      attribute_youtube_video: "video id1",
      attribute_value_description_id: "1,2,3,4",
      attribute_value: [
        {
          attributeValue: "Bombay Wear",
          isSelected: false
        },
        {
          attributeValue: "Terry Cotton",
          isSelected: false
        },
        {
          attributeValue: "Nylon",
          isSelected: false
        },
        {
          attributeValue: "Sutti",
          isSelected: false
        }
      ],
      attribute_description: [
        {
          attributeDescription: "Highest Quality"
        },
        {
          attributeDescription: "Average Choosed"
        },
        {
          attributeDescription: "Silky Cloth"
        },
        {
          attributeDescription: "Old Aged "
        }
      ],
      price_per_m: null
    },
    {
      product_type_id: "1",
      attribute_id: "2",
      attribute_name: "Achara Length",
      product_type_method_id: 3,
      product_type_type_id: 1,
      product_type_unit_id: 3,
      attribute_gif_url: "https://i.imgur.com/cr0LVC7.jpg",
      attribute_youtube_video: "video id2",
      attribute_value_description_id: "5,6,7,8,9,12,13,14",
      attribute_value: [
        {
          attributeValue: "3.5",
          isSelected: false
        },
        {
          attributeValue: "4",
          isSelected: false
        },
        {
          attributeValue: "4.5",
          isSelected: false
        },
        {
          attributeValue: "5",
          isSelected: false
        },
        {
          attributeValue: "5.5",
          isSelected: false
        },
        {
          attributeValue: "6",
          isSelected: false
        },
        {
          attributeValue: "6.5",
          isSelected: false
        },
        {
          attributeValue: "7 ",
          isSelected: false
        }
      ],
      attribute_description: [
        {
          attributeDescription: "small"
        },
        {
          attributeDescription: "medium"
        },
        {
          attributeDescription: "standard"
        },
        {
          attributeDescription: "standard"
        },
        {
          attributeDescription: "large"
        },
        {
          attributeDescription: "large"
        },
        {
          attributeDescription: "very large"
        },
        {
          attributeDescription: "very large"
        }
      ],
      price_per_m: "270"
    }
  ],
  [
    {
      product_type_id: "2",
      attribute_id: "4",
      attribute_name: "Khadki Cloth",
      product_type_method_id: 3,
      product_type_type_id: 3,
      product_type_unit_id: 3,
      attribute_gif_url: "https://i.imgur.com/cr0LVC7.jpg",
      attribute_youtube_video: "video id4",
      attribute_value_description_id: "10,11",
      attribute_value: [
        {
          attributeValue: "Hand Made",
          isSelected: false
        },
        {
          attributeValue: "Ready Made",
          isSelected: false
        }
      ],
      attribute_description: [
        {
          attributeDescription: "Mostly Choosed"
        },
        {
          attributeDescription: "Low Cost"
        }
      ],
      price_per_m: null
    },
    {
      product_type_id: "2",
      attribute_id: "6",
      attribute_name: "khadki Color",
      product_type_method_id: 3,
      product_type_type_id: 3,
      product_type_unit_id: 4,
      attribute_gif_url: "https://i.imgur.com/cr0LVC7.jpg",
      attribute_youtube_video: "video id 6",
      attribute_value_description_id: "19,20",
      attribute_value: [
        {
          attributeValue: "white",
          isSelected: false
        },
        {
          attributeValue: "mix",
          isSelected: false
        }
      ],
      attribute_description: [
        {
          attributeDescription: "poorly choosed"
        },
        {
          attributeDescription: "highly choosed"
        }
      ],
      price_per_m: null
    }
  ],
  [
    {
      product_type_id: "3",
      attribute_id: "5",
      attribute_name: "Blouse Size",
      product_type_method_id: 3,
      product_type_type_id: 1,
      product_type_unit_id: 3,
      attribute_gif_url: "https://i.imgur.com/cr0LVC7.jpg",
      attribute_youtube_video: "video id",
      attribute_value_description_id: "15,16,17,18",
      attribute_value: [
        {
          attributeValue: "15",
          isSelected: false
        },
        {
          attributeValue: "20",
          isSelected: false
        },
        {
          attributeValue: "25",
          isSelected: false
        },
        {
          attributeValue: "30",
          isSelected: false
        }
      ],
      attribute_description: [
        {
          attributeDescription: "small"
        },
        {
          attributeDescription: "medium"
        },
        {
          attributeDescription: "standard"
        },
        {
          attributeDescription: "large"
        }
      ],
      price_per_m: null
    }
  ]
];

const productTypes = [
  {
    product_type_name: "Achara Cloth"
  },
  {
    product_type_name: "Khadki "
  },
  {
    product_type_name: "Blouse"
  }
];
export default function App() {
  return (
    <div className="App">
      <ProductTypesTable
        productTypes={productTypes}
        productTypesSizeAttributesList={productTypesSizeAttributesList}
      ></ProductTypesTable>
    </div>
  );

  function ProductTypesTable({ productTypes, productTypesSizeAttributesList }) {
    const [activeMainTab, setActiveMainTab] = useState(0);
    // 新增全局存储:key为product_type_id,值为{attribute_id: 选中索引}的对象
    const [attrSelectedMap, setAttrSelectedMap] = useState({});

    // 更新属性选中值的方法
    const updateAttrSelected = (productTypeId, attrId, index) => {
      setAttrSelectedMap(prev => ({
        ...prev,
        [productTypeId]: {
          ...prev[productTypeId],
          [attrId]: index
        }
      }))
    }

    return (
      <div className="size__table__product__types__attributes__container">
        <div className="size__table__product__types__container">
          {productTypes.map((obj, index) => {
            return (
              <ProductTypesTableTabs
                key={index}
                index={index}
                name={obj.product_type_name}
              ></ProductTypesTableTabs>
            );
          })}
        </div>
        <div className="size__table__attributes">
          <ProductSizeAttribtutes
            productTypesSizeAttributes={productTypesSizeAttributesList[activeMainTab]}
            attrSelectedMap={attrSelectedMap}
            updateAttrSelected={updateAttrSelected}
          ></ProductSizeAttribtutes>
        </div>
        <div className="total__price__of__size__attribute">
          Total {productTypes[activeMainTab].product_type_name} Price:
        </div>
      </div>
    );

    function ProductTypesTableTabs({ name, index }) {
      return (
        <div
          className={`size__table__product__types inactive_btn__size__table ${
            index === activeMainTab && "active_btn__size__table"
          }`}
          onClick={() => {
            setActiveMainTab(index);
          }}
        >
          <div className="size__table__product__types__title">{name}</div>
        </div>
      );
    }
  }

  // product size attribute overall boxes
  function ProductSizeAttribtutes({ productTypesSizeAttributes, attrSelectedMap, updateAttrSelected }) {
    return (
      <div>
        {productTypesSizeAttributes.map((obj) => {
          return (
            <ProductSizeAttributesSingle
              key={obj.attribute_id}
              obj={obj}
              attrSelectedMap={attrSelectedMap}
              updateAttrSelected={updateAttrSelected}
            ></ProductSizeAttributesSingle>
          );
        })}
      </div>
    );
  }

  function ProductSizeAttributesSingle({ obj, attrSelectedMap, updateAttrSelected }) {
    return (
      <div>
        <div className="select__size__attributes__container">
          <div className="select__title__image">
            <div className="attribute_name"> SELECT {obj.attribute_name}:</div>
            <div className="gif__url__video__conversion">
              <div>
                <img src={obj.attribute_gif_url} alt="gif url" height="70px" />
              </div>
            </div>
          </div>

          <IndividualSelectingAttributeContainer
            obj={obj}
            attrSelectedMap={attrSelectedMap}
            updateAttrSelected={updateAttrSelected}
          ></IndividualSelectingAttributeContainer>
        </div>

        <hr />
      </div>
    );
  }

  function IndividualSelectingAttributeContainer({ obj, attrSelectedMap, updateAttrSelected }) {
    // 移除局部选中状态,改用父层传递的全局状态,默认值为0
    const selectedIndex = attrSelectedMap[obj.product_type_id]?.[obj.attribute_id] ?? 0;
    return (
      <div className="selecting_size_containers_div">
        {obj.attribute_value.map((objAv, index) => {
          return (
            <SelectingSizeAttributesContainer
              key={index}
              attribute_value={objAv.attributeValue}
              container={objAv}
              selectedIndex={selectedIndex}
              index={index}
              onSelect={() => updateAttrSelected(obj.product_type_id, obj.attribute_id, index)}
            ></SelectingSizeAttributesContainer>
          );
        })}
      </div>
    );
  }

  // selecting size attributes container
  function SelectingSizeAttributesContainer({
    attribute_value,
    container,
    selectedIndex,
    index,
    onSelect
  }) {
    return (
      <div
        className={`selecting_size_containers_div_tab ${
          index === selectedIndex && "activated_selected_containers"
        }`}
        onClick={onSelect}
      >
        <div className="attribute_value">{container.attributeValue}</div>
      </div>
    );
  }
}

修改后每个主标签下的属性选中状态会独立存储,切换主标签不会出现状态串扰,切回之前的主标签也会保留之前的选中记录。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.23 21:15:04