子组件通过父组件过滤时如何保持状态?
Posted
技术标签:
【中文标题】子组件通过父组件过滤时如何保持状态?【英文标题】:How to maintain state of child components when they are filtered through the parent component? 【发布时间】:2020-07-21 01:29:16 【问题描述】:我正在使用 create react app 构建一个小应用程序来提高我的反应知识,但现在卡在状态管理上。
应用程序通过父组件上的 JSON 数据进行映射,并打印 6 个“图像卡片”作为子组件,其中包含一组“标签”来描述它以及作为道具传递的其他数据(url、标题等)。
每张卡片都有一个输入,您可以在现有列表中添加更多标签。
在父组件上有一个输入,可用于通过标签过滤卡片。 (仅过滤默认标签而不是添加到卡片的新标签)。
我想要实现的是在每张卡被过滤时保持其状态。目前发生的情况是,如果我向卡片添加新标签并使用多个标签进行过滤,则只有初始过滤的卡片包含新标签,其余的会使用其默认标签重新渲染。谁能告诉我哪里出错了,谢谢。
如果它使事情变得更容易,我的项目也可以被克隆 https://github.com/sai-re/assets_tag
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"
]
]
父组件
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;
子组件
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 />
<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;
【问题讨论】:
那些输入值必须处于父状态。因此,当它们从渲染中过滤出来时,它会被记住。会不会是这个问题? 【参考方案1】:渲染所有项目,即使它们被过滤掉,并且只隐藏使用 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 /> ));
;
在项目本身中,使用 hide
属性使用 style
属性或 css 类通过 CSS 隐藏项目:
return (
<div className="item" style= display: props.hide ? 'none' : 'block' >
您还可以通过始终创建 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 />
);
)
;
【讨论】:
【参考方案2】:您面临的问题是消失的卡牌被卸载,这意味着它们的状态丢失了。最好的解决方案是将添加到卡片的新自定义标签保留在父组件中,因此无论卡片是否已安装,它都是持久的。以下是修改后的文件:
父组件
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;
子组件
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 />
<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;
希望这会有所帮助,如果有什么不清楚的地方,我可以添加一些 cmets :)
【讨论】:
嗨,亚当,我尝试将其集成到其中,但是当我向卡片添加新标签时,会添加一个空字符串而不是输入。尽管您的解决方案在过滤后确实保持相同的状态,但我是否正确地说您基本上将卡片状态保留在父级并使用回调通过道具更新它? 这是正确的,父级保留所有状态,回调addCustomTag
和removeCustomTag
用于修改此状态
我真的不确定,为什么将一个空字符串推送到标签而不是您的输入。也许尝试将state.tag
记录在handleClick
和tag
内部addChildrenTag
以检查所有内容是否正确传递?以上是关于子组件通过父组件过滤时如何保持状态?的主要内容,如果未能解决你的问题,请参考以下文章
在组件之间路由时如何保持 React 新的 Context API 状态?