I am building a small app using create react app to improve my react knowledge but now stuck with state management.
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.
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. 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. 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
data.json example
{
"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;
Render all items, even if they are filtered out, and just hide the items filtered out using 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:
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
:
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} />
);
})
};