[英]How to maintain state of child components when they are filtered through the parent component?
I am building a small app using create react app to improve my react knowledge but now stuck with state management.我正在使用 create react app 构建一个小应用程序来提高我的反应知识,但现在坚持使用 state 管理。
The apps maps through JSON data on the parent component and prints 6 "image cards" as child components with an array of "tags" to describe it and other data(url, titles etc..) passed as props.应用程序通过父组件上的 JSON 数据进行映射,并打印 6 个“图像卡”作为子组件,其中包含一组“标签”来描述它以及作为道具传递的其他数据(网址、标题等)。
Each card has an input which you can add more tags to the existing list.每张卡片都有一个输入,您可以将更多标签添加到现有列表中。
On the parent component there is an input which can be used to filter the cards through the tags.在父组件上有一个输入,可用于通过标签过滤卡片。 (only filters through default tags not new ones added to card).
(仅过滤默认标签而不是添加到卡片的新标签)。
What I am trying to achieve is maintaining the state of each card when it gets filtered.我想要实现的是在过滤后保持每张卡的 state 。 Currently what happens is if I add new tags to the cards and filter using multiple tags, only the initial filtered cards contain the new tags, the rest get re-rendered with their default tags.
目前发生的情况是,如果我向卡片添加新标签并使用多个标签进行过滤,则只有初始过滤的卡片包含新标签,rest 会使用其默认标签重新呈现。 Can someone tell me where I am going wrong, thanks.
谁能告诉我哪里出错了,谢谢。
My project can also be cloned if it makes things easier https://github.com/sai-re/assets_tag如果它使事情变得更容易,我的项目也可以被克隆https://github.com/sai-re/assets_tag
data.json example data.json 示例
{
"assets": [
{
"url": "https://images.unsplash.com/photo-1583450119183-66febdb2f409?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=200&ixlib=rb-1.2.1&q=80&w=200",
"title": "Car",
"tags": [
{ "id": "USA", "text": "USA" },
{ "id": "Car", "text": "Car" }
],
"suggestions": [
{ "id": "Colour", "text": "Colour" },
{ "id": "Motor", "text": "Motor" },
{ "id": "Engineering", "text": "Engineering" }
]
},
{
"url": "https://images.unsplash.com/photo-1582996269871-dad1e4adbbc7?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=200&ixlib=rb-1.2.1&q=80&w=200",
"title": "Plate",
"tags": [
{ "id": "Art", "text": "Art" },
{ "id": "Wood", "text": "Wood" },
{ "id": "Spoon", "text": "Spoon" }
],
"suggestions": [
{ "id": "Cutlery", "text": "Cutlery" },
{ "id": "Serenity", "text": "Serenity" }
]
}
]
}
Parent component父组件
import React, {useState} from 'react';
import Item from './Item'
import data from '../../data.json';
import './Assets.scss'
function Assets() {
const [state, updateMethod] = useState({tag: "", tags: []});
const printList = () => {
//if tag in filter has been added
if (state.tags.length > 0) {
return data.assets.map(elem => {
//extract ids from obj into array
const dataArr = elem.tags.map(item => item.id);
const stateArr = state.tags.map(item => item.id);
//check if tag is found in asset
const doesTagExist = stateArr.some(item => dataArr.includes(item));
//if found, return asset
if (doesTagExist) return <Item key={elem.title} data={elem} />;
})
} else {
return data.assets.map(elem => (<Item key={elem.title} data={elem} /> ));
}
};
const handleClick = () => {
const newTag = {id: state.tag, text: state.tag};
const copy = [...state.tags, newTag];
if (state.tag !== "") updateMethod({tag: "", tags: copy});
}
const handleChange = e => updateMethod({tag: e.target.value, tags: state.tags});
const handleDelete = i => {
const copy = [...state.tags];
let removed = copy.filter((elem, indx) => indx !== i);
updateMethod({tag: state.tag, tags: removed});
}
return (
<div className="assets">
<div className="asset__filter">
<h3>Add tags to filter</h3>
<ul className="asset__tag-list">
{state.tags.map((elem, i) => (
<li className="asset__tag" key={`${elem.id}_${i}`} >
{elem.text}
<button className="asset__tag-del" onClick={() => handleDelete(i)}>x</button>
</li>
))}
</ul>
<input
type="text"
value={state.tag}
onChange={handleChange}
placeholder="Enter new tag"
className="asset__tag-input"
/>
<button className="asset__btn" onClick={handleClick}>Add</button>
</div>
<div className="item__list-holder">
{printList()}
</div>
</div>
);
}
export default Assets;
Child component子组件
import React, {useState, useEffect} from 'react';
function Item(props) {
const [state, updateMethod] = useState({tag: "", tags: []});
const handleClick = () => {
//create new tag from state
const newTag = {id: state.tag, text: state.tag};
//create copy of state and add new tag
const copy = [...state.tags, newTag];
//if state is not empty update state with new tags
if (state.tag !== "") updateMethod({tag: "", tags: copy});
}
const handleChange = e => updateMethod({tag: e.target.value, tags: state.tags});
const handleDelete = i => {
//copy state
const copy = [...state.tags];
//filter out tag to be deleted
let removed = copy.filter((elem, indx) => indx !== i);
//add updated tags to state
updateMethod({tag: state.tag, tags: removed});
}
useEffect(() => {
console.log("item rendered");
//when first rendered, add default tags from json to state
updateMethod({tag: "", tags: props.data.tags});
}, [props.data.tags]);
const assets = props.data;
return (
<div className="item">
<img src={assets.url} alt="assets.title"/>
<h1 className="item__title">{assets.title}</h1>
<div className="item__tag-holder">
<ul className="item__tag-list">
{state.tags.map((elem, i) => (
<li className="item__tag" key={`${elem.id}_${i}`} >
{elem.text}
<button className="item__tag-del" onClick={() => handleDelete(i)}>x</button>
</li>
))}
</ul>
<input
type="text"
value={state.tag}
onChange={handleChange}
placeholder="Enter new tag"
className="item__tag-input"
/>
<button className="item__btn" onClick={handleClick}>Add</button>
</div>
</div>
);
}
export default Item;
The problem that you are facing is that the cards that disappear are unmounted, meaning that their state is lost.您面临的问题是消失的卡已卸载,这意味着它们的 state 丢失了。 The best solution is keeping the new custom tags you add to cards in the parent component, so it's persistent, no matter if the card is mounted or not.
最好的解决方案是将添加到卡片的新自定义标签保留在父组件中,因此无论卡片是否已安装,它都是持久的。 Here are the modified files:
以下是修改后的文件:
Parent component父组件
import React, {useState} from 'react';
import Item from './Item'
import data from '../../data.json';
import './Assets.scss'
function Assets() {
const [state, updateMethod] = useState({tag: "", tags: []});
const [childrenTags, setChildrenTags] = useState(data.assets.map(elem => elem.tags));
const addChildrenTag = (index) => (tag) => {
let newTags = Array.from(childrenTags)
newTags[index] = [...newTags[index], tag]
setChildrenTags(newTags)
}
const removeChildrenTag = (index) => (i) => {
let newTags = Array.from(childrenTags)
newTags[index] = newTags[index].filter((elem, indx) => indx !== i)
setChildrenTags(newTags)
}
const printList = () => {
//if tag in filter has been added
if (state.tags.length > 0) {
return data.assets.map((elem, index) => {
//extract ids from obj into array
const dataArr = elem.tags.map(item => item.id);
const stateArr = state.tags.map(item => item.id);
//check if tag is found in asset
const doesTagExist = stateArr.some(item => dataArr.includes(item));
//if found, return asset
if (doesTagExist)
return (
<Item
key={elem.title}
data={elem}
customTags={childrenTags[index]}
addCustomTag={addChildrenTag(index)}
removeCustomTag={removeChildrenTag(index)}
/>
)
})
} else {
return data.assets.map((elem, index) => (
<Item
key={elem.title}
data={elem}
customTags={childrenTags[index]}
addCustomTag={addChildrenTag(index)}
removeCustomTag={removeChildrenTag(index)}
/>
));
}
};
const handleClick = () => {
const newTag = {id: state.tag, text: state.tag};
const copy = [...state.tags, newTag];
if (state.tag !== "") updateMethod({tag: "", tags: copy});
}
const handleChange = e => updateMethod({tag: e.target.value, tags: state.tags});
const handleDelete = i => {
const copy = [...state.tags];
let removed = copy.filter((elem, indx) => indx !== i);
updateMethod({tag: state.tag, tags: removed});
}
return (
<div className="assets">
<div className="asset__filter">
<h3>Add tags to filter</h3>
<ul className="asset__tag-list">
{state.tags.map((elem, i) => (
<li className="asset__tag" key={`${elem.id}_${i}`} >
{elem.text}
<button className="asset__tag-del" onClick={() => handleDelete(i)}>x</button>
</li>
))}
</ul>
<input
type="text"
value={state.tag}
onChange={handleChange}
placeholder="Enter new tag"
className="asset__tag-input"
/>
<button className="asset__btn" onClick={handleClick}>Add</button>
</div>
<div className="item__list-holder">
{printList()}
</div>
</div>
);
}
export default Assets;
Child component子组件
import React, {useState, useEffect} from 'react';
function Item(props) {
const [state, updateMethod] = useState({tag: ""});
cosnst tags = props.customTags
cosnst addCustomTag = props.addCustomTag
cosnst removeCustomTag = props.removeCustomTag
const handleClick = () => {
if (state.tag !== "") addCustomTag(state.tag);
}
const handleChange = e => updateMethod({tag: e.target.value});
const handleDelete = i => {
removeCustomTag(i);
}
const assets = props.data;
return (
<div className="item">
<img src={assets.url} alt="assets.title"/>
<h1 className="item__title">{assets.title}</h1>
<div className="item__tag-holder">
<ul className="item__tag-list">
{tags.map((elem, i) => (
<li className="item__tag" key={`${elem.id}_${i}`} >
{elem.text}
<button className="item__tag-del" onClick={() => handleDelete(i)}>x</button>
</li>
))}
</ul>
<input
type="text"
value={state.tag}
onChange={handleChange}
placeholder="Enter new tag"
className="item__tag-input"
/>
<button className="item__btn" onClick={handleClick}>Add</button>
</div>
</div>
);
}
export default Item;
Hope this helps, I can add some comments if anything is unclear:)希望这会有所帮助,如果有任何不清楚的地方,我可以添加一些评论:)
Render all items, even if they are filtered out, and just hide the items filtered out using CSS ( display: none
):渲染所有项目,即使它们被过滤掉,只需隐藏使用 CSS 过滤掉的项目(
display: none
):
const printList = () => {
//if tag in filter has been added
if (state.tags.length > 0) {
// create a set of tags in state once
const tagsSet = new Set(state.tags.map(item => item.id));
return data.assets.map(elem => {
//hide if no tag is found
const hideElem = !elem.tags.some(item => tagsSet.has(item.id));
//if found, return asset
return <Item key={elem.title} data={elem} hide={hideElem} />;
})
} else {
return data.assets.map(elem => (<Item key={elem.title} data={elem} /> ));
}
};
And in the Item itself, use the hide
prop to hide the item with CSS using the style
attribute or a css class:在项目本身中,使用
hide
道具使用style
属性或 css class 隐藏项目:
return (
<div className="item" style={{ display: props.hide ? 'none' : 'block' }}>
You can also simplify printList()
a bit more, by always creating the Set, even if state.tags
is empty, and if it's empty hideElem
would be false
:您还可以通过始终创建 Set 来进一步简化
printList()
,即使state.tags
为空,如果它为空hideElem
将为false
:
const printList = () => {
const tagsSet = new Set(state.tags.map(item => item.id));
return data.assets.map(elem => {
//hide if state.tags is empty or no selected tags
const hideElem = tagsSet.size > 0 && !elem.tags.some(item => tagsSet.has(item.id));
//if found, return asset
return (
<Item key={elem.title} data={elem} hide={hideElem} />
);
})
};
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.