I am new to React and trying to learn more by creating projects. I made an API call to display some images to the page and I would like to create a like button/icon for each image that changes to red when clicked. However, when I click one button all of the icons change to red. I believe this may be related to the way I have set up my state, but can't seem to figure out how to target each item individually. Any insight would be much appreciated.
`
//store api data
const [eventsData, setEventsData] = useState([]);
//state for like button
const [isLiked, setIsLiked] = useState(false);
useEffect(() => {
axios({
url: "https://app.ticketmaster.com/discovery/v2/events",
params: {
city: userInput,
countryCode: "ca",
},
})
.then((response) => {
setEventsData(response.data._embedded.events);
})
.catch((error) => {
console.log(error)
});
});
//here i've tried to filter and target each item and when i
console.log(event) it does render the clicked item, however all the icons
change to red at the same time
const handleLikeEvent = (id) => {
eventsData.filter((event) => {
if (event.id === id) {
setIsLiked(!isLiked);
}
});
};
return (
{eventsData.map((event) => {
return (
<div key={event.id}>
<img src={event.images[0].url} alt={event.name}></img>
<FontAwesomeIcon
icon={faHeart}
className={isLiked ? "redIcon" : "regularIcon"}
onClick={() => handleLikeEvent(event.id)}
/>
</div>
)
`
Store likes as array of ids
const [eventsData, setEventsData] = useState([]);
const [likes, setLikes] = useState([]);
const handleLikeEvent = (id) => {
setLikes(likes.concat(id));
};
return (
<>
{eventsData.map((event) => {
return (
<div key={event.id}>
<img src={event.images[0].url} alt={event.name}></img>
<FontAwesomeIcon
icon={faHeart}
className={likes.includes(event.id) ? "redIcon" : "regularIcon"}
onClick={() => handleLikeEvent(event.id)}
/>
</div>
);
})}
</>
);
Your issue is with your state, isLiked is just a boolean true or false, it has no way to tell the difference between button 1, or button 2 and so on, so you need a way to change the css property for an individual button, you can find one such implementation by looking Siva's answer, where you store their ids in an array
Related
Im Having a Table which has multiple records and Filter component with the Search Bar. What im trying to do is Based on the value selected by the user from all the filters i have pass those arrays to parent and form an object,
Im having 3 components here,
1)Parent : Data
export default function Data(props) {
const [domain, setDomain] = useState([]);
const [fileType, setFileType] = useState([]);
const [entity, setEntity] = useState(["Patents"]);
const [year, setYear] = useState({});
//This is the search bar state
const [keywords, setKeywords] = useState([]);
//based on the filter values im calling the API to get the records for table based on the value selected by the user from my filer
useEffect(() => {
const fetchResults = async (projectid) => {
const url = props.apiURL.rpaapiurl + "/search";
console.log("fetchData called-->" + url);
const resultsObj = {
projectId: projectid,
filter: {
domain: domain,
fileType: fileType,
entity: entity,
},
};
const response = await fetch(url, {
method: "POST",
body: JSON.stringify(resultsObj),
headers: {
"Content-Type": "application/json",
},
});
const data = await response.json();
console.log("All data-->", data);
setResults(data);
};
fetchResults(5);
}, [domain, fileType, entity]);
const handleFileType = (fileTypeArray) => {
setFileType(fileTypeArray);
};
return (
<Item1>
<Dropdown onChangeFileType={(FileTypeFilteredArray) => handleFileType(FileTypeFilteredArray)} ></Dropdown>
</Item1>
<Item2>
<Table
Data={dataresults}
Attributes={resultTable}
entitytypeHandler={props.entitytypeHandler}
></Table>
</Item2>
)
From the data parent component im passing the hadler which will return updated array from the child and im setting it to state.
2)Child : Dropdown
export default function Dropdown(props) {
return (
<FilterItem>
<Input
type="search"
placeholder="Search in title, description, keywords"
></Input>
<Filter1></Filter1>
<Filetr2></Filetr2>
<ContentFormat
onChangeFileType={props.onChangeFileType}
></ContentFormat>
<Filter4></Filter4>
<Filter5></Filter5>
<TextWrap>
<P text="End year" fontSize="14px" color="#454545"></P>
<KeywordImg src={droparrow} />
</TextWrap>
</FilterItem>
)}
Nothing special here since we can not skip a component passing the same thing to nested child,
Nested Child : ContentFormat
export default function ContentFormat(props) {
const [isDisplay, setIsDisplay] = useState("false");
const array = ["HTML", "PDF"];
const toggle = () => {
setIsDisplay(!isDisplay);
};
let fileTypeArray = [];
const handleSelection = (event) => {
const value = event.target.value;
console.log("value-->", +value);
if (event.target.checked == true) {
fileTypeArray.push(value);
console.log("if fileTypeArray-->", fileTypeArray);
} else if (fileTypeArray.length > 0) {
fileTypeArray = fileTypeArray.filter((element) => {
console.log("element-->", +element);
if (event.target.value !== element) return element;
});
console.log("else fileTypeArray-->", fileTypeArray);
}
console.log("function fileTypeArray-->", fileTypeArray);
};
const applyClickHandler = () => {
console.log("Applied fileTypeArray-->", fileTypeArray);
props.onChangeFileType(fileTypeArray);
};
return (
<div>
<DropContent>
<DropButton onClick={toggle}>
{" "}
<P text="By Content Format" fontSize="14px" color="#454545"></P>
<KeywordImg src={droparrow} />
</DropButton>
<ContextWrapper style={{ display: isDisplay ? "none" : "block" }}>
<P
text="Filter by Extension types"
fontSize="18px"
color="#ACACAC"
textAlign="center"
padding="22px 32px 14px"
></P>
<DropScroll className="sl-style-3">
{array.map((item, index) => {
return (
<ContextItem key={index}>
<DropList
onHandleSelection={handleSelection}
text={item}
value={item}
></DropList>
</ContextItem>
);
})}
</DropScroll>
<ApplyButton onClick={applyClickHandler}>
<P text="Apply" fontSize="16px" color="#fff" textAlign="center"></P>
</ApplyButton>
</ContextWrapper>
</DropContent>
</div>
);
}
4)DropList
export default function DropList(props) {
const changeHandler = (e) => {
console.log(e);
props.onHandleSelection(e);
};
return (
<div>
<div className="">
<TickBox
type="checkbox"
id={props.id}
name={props.name}
value={props.value}
onChange={(e) => {
changeHandler(e);
}}
/>
{props.text}
</div>
</div>
);
}
I'm getting the updated array on click of apply button in the parent but if user un-selects any check box the it deleting the complete array
In data i have to form the object base on the state array passed by all the filters, i tried for the one filter as above but its not working can any one suggest better way to do it,
Because here handling one filter is default and i have to do it for total 5 filters
So any suggestion or one common component for all the filters
Im not sure whether i should be asking these kinda questions or not since I'm very at posting the right questios but pardon me if its wrong question or the way of asking is wrong,
Any help would be appricited.
I have a React notes app that has a delete button, and a state for user confirmation of deletion.
Once user confirms, the 'isConfirmed' state is updated to true and deletes the item from MongoAtlas and removes from notes array in App.jsx.
The problem is, the note that takes the index (through notes.map() in app.jsx I'm assuming) of the deleted notes position in the array has the 'isConfirmed' state set to true without calling setState. Thus, bugging out my delete button to not work for that specific note until page refresh.
I've included relevant code from my Delete Component:
function DeletePopup(props) {
const mountedRef = useRef(); //used to stop useEffect call on first render
const [isConfirmed, setIsConfirmed] = useState(false);
const [show, setShow] = useState(false);
function confirmDelete() {
// console.log("user clicked confirm");
setIsConfirmed(true);
// console.log(isConfirmed);
handleClose();
}
useEffect(() => {
// console.log("delete useEffect() run");
if (mountedRef.current) {
props.deleteNote(isConfirmed);
}
mountedRef.current = true;
}, [isConfirmed]);
Note Component:
function Note(props) {
function deleteNote(isConfirmed) {
props.deleteNote(props.id, { title: props.title, content: props.content }, isConfirmed);
console.log("note.deleteNote ran with confirmation boolean: " + isConfirmed);
}
return <Draggable
disabled={dragDisabled}
onStop={finishDrag}
defaultPosition={{ x: props.xPos, y: props.yPos }}
>
<div className='note'>
<h1>{props.title}</h1>
<p>{props.content}</p>
<button onClick={handleClick}>
{dragDisabled ? <LockIcon /> : <LockOpenIcon />}
</button>
<EditPopup title={props.title} content={props.content} editNote={editNote} />
<DeletePopup deleteNote={deleteNote} />
</div>
</Draggable>
}
App Component:
function App() {
const [notes, setNotes] = useState([]);
function deleteNote(id, deleteNote, isConfirmed) {
if (!isConfirmed) return;
axios.post("/api/note/delete", deleteNote)
.then((res) => setNotes(() => {
return notes.filter((note, index) => {
return id !== index;
});
}))
.catch((err) => console.log(err));
}
return (
<div id="bootstrap-override">
<Header />
<CreateArea
AddNote={AddNote}
/>
{notes.map((note, index) => {
return <Note
key={index}
id={index}
title={note.title}
content={note.content}
xPos={note.xPos}
yPos={note.yPos}
deleteNote={deleteNote}
editNote={editNote}
/>
})}
<Footer />
</div>);
}
I've tried inserting log statements everywhere and can't figure out why this is happening.
I appreciate any help, Thanks!
EDIT: I changed my Notes component to use ID based on MongoAtlas Object ID and that fixed the issue. Thanks for the help!
This is because you are using the index as key.
Because of that when you delete an element you call the Array.filter then you the elements can change the index of the array which when React tries to rerender the notes and as the index changes it cannot identify the note you've deleted.
Try using a unique id (e.g. an id from the database or UUID) as a key instead.
I hope it solves your problem!
I am currently get this duplicated key warning "Warning: Encountered two children with the same key". However, I am unsure where this duplication of key comes from. I am using the fileData id as my key which should be unique as it is firebase generated id. Therefore, I am not so sure what is happening behind here.
Here are my codes below and the warning I get.
MultimediaDetails.js
import React, { useEffect, useState } from "react";
import * as AiIcons from "react-icons/ai";
import * as FaIcons from "react-icons/fa";
import { database } from "../../../firebase";
import ViewImageFileModal from "../../modals/multimediaModals/view/ViewImageFileModal";
/**
* It's a component that displays audio, video, and image files
* #param props - The props object that is passed to the component.
* #returns The MultimediaDetails component is being returned.
*/
const MultimediaDetails = (props) => {
/* Destructuring the props object. */
const { pId } = props;
/* Setting the state of the component. */
const [imageData, setImageData] = useState([]);
const [imageMessage, setImageMessage] = useState(true);
const userType = JSON.parse(localStorage.getItem("admin") ?? false);
// Modal Variables
const [showViewImageModal, setShowViewImageModal] = useState(false);
const [fileData, setFileData] = useState(Object);
/**
* When the user clicks on the audio, video, or image file, the file data is set and the modal is
* toggled.
* #param obj
*/
const viewImageFile = (obj) => {
setFileData(obj);
toggleViewImageModal();
};
/* The function to toggle modal states */
const toggleAddImageModal = () => setShowAddImageModal((p) => !p);
const toggleViewImageModal = () => setShowViewImageModal((p) => !p);
useEffect(() => {
/* Query data from database and listening for changes. */
const imageQuery = database.portfolioRef.doc(pId).collection("images");
const unsubscribeImage = imageQuery.onSnapshot((snapshot) => {
if (snapshot.docs.length !== 0) {
setImageMessage(false);
setImageData(
snapshot.docs.map((doc) => ({ ...doc.data(), id: doc.id }))
);
} else {
setImageMessage(true);
}
});
return () => {
unsubscribeImage();
};
}, [pId]);
return (
<div className="multimedia-section">
<div id="image-section">
<div id="image-header">
<h6>
<u>Images</u>
</h6>
{userType ? (
<button className="addbtn" onClick={() => toggleAddImageModal()}>
<AiIcons.AiOutlinePlus /> Add Image File
</button>
) : (
<></>
)}
</div>
<div id="image-content" className="multimedia-flex">
{imageMessage ? (
<p>There is not existing images for this portfolio.</p>
) : (
<div>
{imageData.map((doc) => (
<button
key={doc.id}
className="fileBtn"
onClick={() => viewImageFile(doc)}
>
<FaIcons.FaImage /> {doc.imageName}
</button>
))}
</div>
)}
</div>
</div>
<ViewImageFileModal
show={showViewImageModal}
toggleModal={toggleViewImageModal}
pId={pId}
data={fileData}
key={fileData.id}
/>
</div>
);
};
export default MultimediaDetails;
The initialised values for the Modal.
/* Setting the initial state of the component. */
const valueState = {
name: '',
description: ''
}
const { currentUser } = useAuth();
const [formStateDisabled, setFormStateDisabled] = useState(true);
const [deleteState, setDeleteState] = useState(false);
const [message, setMessage] = useState('');
const [imageUrl, setImageUrl] = useState("");
const [loadForm, setLoadForm] = useState(false)
const [view, setView] = useState(false);
/* Destructuring the props object. */
const { show, toggleModal } = props;
const { handleChange, handleSubmit, values, errors, loading } =
useForm(validateUpdate, valueState, handleUpdate);
useEffect(() => {
if (Object.keys(props.data).length !== 0) {
values.name = props.data.imageName;
values.description = props.data.imageDesc;
setLoadForm(true);
}
}, [])
The warning I get (Shown Below), each time I click on the modal button to open the button, I noticed the warning actually repeats twice, and when I close it, it repeats another 2 times making it 4. I am not sure what is the cause of this, please help! Thank you!
Updates of trials
I only have 4 rows of data, all of which has its own unique id. Therefore I am unsure of where the duplicated key came from. However, if I remove the modal key "fileData.id" this warning would disappear. However, my component state will not reset and there will be a lot of props data issue that would surface. Where data for the previously clicked button will appear on the another button. Or the data might not appear at all.
FOR ADDITIONAL INFORMATION:
This is the output for the map buttons
I don't see any duplicates, and I am not sure where the issue is. Is there something I am doing wrong to cause this error. I checked my DB there isn't any data error as well.
Recommended solution
The problem is that your doc.id is repeating.
You are setting the imageData at the imageQuery.onSnapshot callback function, when you run the following code:
setImageData(snapshot.docs.map((doc) => ({ ...doc.data(), id: doc.id })));
What you need to make sure is that doc.id is unique in this context (because you're using this value at the key attribute in your buttons).
That's the correct way to fix it.
Alternative solution
Another way to handle it (as a last resort), is using the following code, where you use the index position of the element at the key attribute:
{imageData.map((doc, index) => (
<button
key={index}
className="fileBtn"
onClick={() => viewImageFile(doc)}
>
<FaIcons.FaImage /> {doc.imageName}
</button>
))}
But this is not recommended according to the React documentation:
We don’t recommend using indexes for keys if the order of items may
change. This can negatively impact performance and may cause issues
with component state. Check out Robin Pokorny’s article for an
in-depth explanation on the negative impacts of using an index as a key.
If you choose not to assign an explicit key to list items then
React will default to using indexes as keys.
Here is an in-depth explanation about why keys are necessary if you’re
interested in learning more.
Instead of doc.id use map item index like bellow. See if it works.
{imageData.map((index, doc) => (
<button
key={index}
className="fileBtn"
onClick={() => viewImageFile(doc)}>
<FaIcons.FaImage /> {doc.imageName}
</button>
))}
This is the culprit:
{imageData.map((doc) => (
<button
key={doc.id}
className="fileBtn"
onClick={() => viewImageFile(doc)}
>
<FaIcons.FaImage /> {doc.imageName}
</button>
))}
The main issue here is that doc.id is duplicate, probably you have duplicate data in your imageData or ou have a faulty data in your database or something that generate a non-unique id.
To easily fix the issue, what you can do is use index of map.
{imageData.map((doc, index) => (
<button
key={index}
className="fileBtn"
onClick={() => viewImageFile(doc)}
>
<FaIcons.FaImage /> {doc.imageName}
</button>
))}
index are always unique. but I suggest you should fix and see why you have duplicate data instead of just bypassing it with an index.
UPDATE
This is quite a hacky solution, but since I can't really pin point what's causing the issue without investigating first hand, let's make it so you don't have to pass a key on the modal.
So instead of storing the object data on the state, store the id instead:
Rename fileData to fileDataId.
const [fileDataId, setFileDataId] = useState(0);
then store the id when clicking the button.
{imageData.map((doc) => (
<button
key={doc.id}
className="fileBtn"
onClick={() => viewImageFile(doc.id)}
>
<FaIcons.FaImage /> {doc.imageName}
</button>
))}
on the Modal, you have to pass the imageData and the selected id, then remove the key:
<ViewImageFileModal
show={showViewImageModal}
toggleModal={toggleViewImageModal}
pId={pId}
list={imageData}
selectedId={fileDataId}
/>
then inside ViewImageFileModal you can declare data as:
const data= props.list.find(image => image.id === props.selectedId);
I just wrap my model with a condition if imageId exist and it works already!
{imageId !== '' &&
<ViewImageFileModal
show={showViewImageModal}
toggleModal={toggleViewImageModal}
imageData={imageData}
key={imageId}
/>
}
I wanted to create a e-commerce web application using react-bootstrap. I want the page to show different item based on category so if the URL is product/men'sclothing i want to filter my array and show only the product that have same category which is men's clothing (my path: product/:category). I already tried to filter my array using .filter method but it didn't work, it still show all product from various category, How can I fix it ?
Categorized product page:
const ProductList = () => {
const { category } = useParams()
const[productList, setProductList]= useState();
useEffect(() =>{
axios.get(`https://fakestoreapi.com/products`).then(res => {
const products = res.data;
setProductList(products);
var filteredCategory =
productList.filter((productList) =>productList.category === {category})
})
}, []);
console.log(productList)
return (
<>
<Row>
<h1> This is {category} paged</h1>
{productList && productList.map(product =>{
const {id, title, price, category,description,image} = product;
return(
<Col lg={3} className="d-flex">
<Card key={id} className="flex-fill productlist">
<Card.Img variant="top" src={image} />
<Card.Body>
<Card.Title>{title}</Card.Title>
<Card.Text>{category}</Card.Text>
<Card.Text>
Current Price: {price}
</Card.Text>
<Button variant="primary">Add to cart</Button>
</Card.Body>
</Card>
</Col>
)
})}
</Row>
</>
)
}
export default ProductList
In the filter function that you have used, try writing it as
productList.filter((product) => product.category === category)
When you write it as {category}, a object is created with key category and the value as the actual value. For example if value of category is shoes, it will create a object, { category: "shoes" }.
You also need to add category in useEffect dependency, to re-fetch products every time category is updated.
First, add a dependency to your UseEffect then remove the bracket inside the filter.
useEffect(() => {
async function getByCategory(){
const req = await fetch(URL):
const res = await req.json();
const filter = res.filter((item) => item.category === category);
setProductList(filter);
}
// check if params exits
if(category){
getByCategory();
}
}, [category]);
Try getting rid of the {} around the category variable in the filter function. The filter function is not inside the return statement and thus plain js (not jsx).
Also, you're never using the array containing the filtered products. I'd suggest to filter the products you get from axios, take the filtered products and put THEM into state with setProductList.
Was not able to test this since I'm on mobile, but give it a try.
Remove the curly braces when comparing the element.
__YOUR CODE
productList.filter((productList) =>productList.category === {category})
__NEW
productList.filter((productList) =>productList.category === category)
You are still listing all products because in your code you are looping through the productList state instead of the new value which come from the filtered data.
{productList && productList.map(product =>{
// this is the way you have defined your map code
}) }
It should be like this
const ProductList = () => {
const { category } = useParams()
const[productList, setProductList]= useState();
useEffect(() =>{
axios.get(`https://fakestoreapi.com/products`).then(res => {
const products = res.data;
setProductList(products);
})
}, []);
let filteredProducts = null;
if(category) {
filteredProducts = productList.filter((productList) => productList.category === category);
} else {
filteredProducts = products;
}
return (
<>
<Row>
<h1> This is {category} paged</h1>
{filteredProducts && filteredProducts.map(product =>{
// some code
})}
</Row>
</>
)
}
export default ProductList
As you can see I define a variable filter Products which contains products related to the category get from the url when It's present otherwise it will use the entire list of products
I want to remove object from my list by clicking on delete icon, but with my logic either everything is deleted from list or nothing, I am not sure how to do it without provided ID to object, I don't have anything unique and I am kinda lost.
Component that renders as many Food as there is in useState:
{cartFood.map((food) => {
return (
<CartFood
key={Math.random()}
foodName={food.foodName}
foodPrice={food.foodPrice}
numberOfPortions={food.numberOfPortions}
cartFood={cartFood}
setCartFood={setCartFood}
/>
);
})}
Logic for removing that particular item that is selected (which is not working and also bad solution since there can be case where you get same name and price twice)
const CartFood = ({
foodName,
foodPrice,
numberOfPortions,
cartFood,
setCartFood,
}) => {
const handleRemoveFood = () => {
setCartFood(
cartFood.filter(
(el) =>
el.foodName &&
el.foodPrice !== cartFood.foodName &&
cartFood.foodPrice
)
);
};
return (
<div className='cartFood-container'>
<p>{foodName}</p>
<p>x{numberOfPortions}</p>
<p>{foodPrice}kn</p>
<p>
<MdDeleteForever
className='cartFood__icon'
onClick={handleRemoveFood}
/>
</p>
</div>
);
};
export default CartFood;
List of objects looks like this:
[{
foodName: "Njoki with sos"
foodPrice: 35
numberOfPortions: 1
},
{
foodName: "Chicken Wingos"
foodPrice: 45
numberOfPortions: 2
}]
Put the index of the item in the array as the id. Pass it as your id.
{cartFood.map((food, index) => {
return (
<CartFood
key={index}
id={index}
foodName={food.foodName}
foodPrice={food.foodPrice}
numberOfPortions={food.numberOfPortions}
cartFood={cartFood}
setCartFood={setCartFood}
/>
);
})}
Use the id to remove the food.
const CartFood = ({
foodName,
foodPrice,
numberOfPortions,
cartFood,
setCartFood,
id,
}) => {
const handleRemoveFood = () => {
setCartFood(cartFood.filter((el) => el.id !== id));
};
return (
<div className='cartFood-container'>
<p>{foodName}</p>
<p>x{numberOfPortions}</p>
<p>{foodPrice}kn</p>
<p>
<MdDeleteForever
className='cartFood__icon'
onClick={handleRemoveFood}
/>
</p>
</div>
);
};
Something like this should work :
const handleRemoveFood = (obj) => {
setCartFood((oldList) => oldList.filter((item) => item.foodName !== obj.foodName));
};
Your button (icon) should call this function with current object data (obj)
A working example : https://codesandbox.io/s/cart-isz6c?file=/index.js
From what I see in your repo:
Just pass the food._id to FoodCard so you access it when you want to add or remove an item from cart:
FoodList.js
const foodList = (typeOfList) =>
typeOfList.map(food => {
return (
<FoodCard
key={food._id}
foodId={food._id}
foodName={food.title}
foodPrice={food.price}
foodPic={food.image}
setCartFood={setCartFood}
cartFood={cartFood}
/>
);
});
FoodCard.js
const handleAddToCard = () => {
setCartFood([
...cartFood,
{
foodId,
foodName,
foodPrice,
numberOfPortions,
},
]);
};
CartFood.js
const handleRemoveFood = () => {
setCartFood(cartFood => cartFood.filter((el) => el.foodId !== foodId));
};
Working example:
You could use useReducer with useContext so you don't have to pass props down manually at every level, check this article for more info
You don't need to pass the cartFood as a property just for updating the state since you can use setState callback:
setCartFood(cartFood => [
...cartFood,
{
foodId,
foodName,
foodPrice,
numberOfPortions,
},
]);