React.js中如何实现多级动态Tabs标签页的状态持久化?
问题根因
你当前的子标签选中状态仅存储在IndividualSelectingAttributeContainer组件的局部useState中,未和对应主标签、属性的唯一标识绑定:
- 切换主标签时React会复用同类型组件实例,导致旧状态被带到新渲染的主标签下,出现状态串扰
- 切回之前操作过的主标签时,局部状态会重置为初始值0,无法保留之前的选中记录
修改方案
我们需要在父层新增全局状态存储每个属性的选中值,用主标签ID、属性ID作为唯一key绑定状态,同时给所有列表渲染项添加唯一key避免组件复用异常。
核心修改点
- 在
ProductTypesTable组件中新增选中状态存储,结构为{ [主标签ID]: { [属性ID]: 选中索引 } } - 移除子组件
IndividualSelectingAttributeContainer的局部选中状态,改用父层传递的状态和更新方法 - 所有
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
相关产品推荐
相关产品推荐

