REACT Duplicated Key Warning - javascript

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}
/>
}

Related

React Stepper with dynamic and responsive steps

I'm building a React stepper with MUI and I wanted to have it be dynamic depending on the data coming in and also to be able to add/remove steps within that dynamic section. I was not able to find any examples or posts on here regarding what I had in mind, and the docs for MUI's stepper don't go anywhere near touching dynamic/responsive steps like this. I've been able to get it pretty far (actually a bit surprised I got it as far as I have), but I'm stuck right at the end. I've set up a generic example on CSB (link below), with the stepper pulling in data objects, displaying them on the dynamic steps, and the add/remove functionality works for the step labels, but not for the content. This is where I can't figure it out, I set up the structure the same for the labels and content, and since is basically just pushing to/filtering out arrays, I'm not finding where the difference is. I figure I'd post it here before messing with it any further. Also, if anyone has any suggestions on how to clean it up, I'd be happy to hear those as well, I'm sure it is a bit messy and heavy handed in getting the job done.
Heres the main Stepper component:
const DynamicStepper = () => {
const [open, setOpen] = useState(false);
const [activeStep, setActiveStep] = useState(0);
const [copiedObjs, setCopiedObjs] = useState([]);
const [copiedLabels, setCopiedLabels] = useState([]);
const [middleContent, setMiddleContent] = useState([]);
const [stepsContent, setStepsContent] = useState([])
//Dialog actions
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
//Stepper actions
const handleNext = () => {
setActiveStep((prevActiveStep) => prevActiveStep + 1);
};
const handleBack = () => {
setActiveStep((prevActiveStep) => prevActiveStep - 1);
}
//Setting up dynamic labels (actual code involves conditional api calls)
useEffect(() => {
if(data) {
setCopiedObjs(data)
}
}, [])
useEffect(() => {
if(copiedObjs.length) {
let labeling = copiedObjs.map((i) => (
i.title
))
setCopiedLabels(labeling)
}
}, [copiedObjs])
//Set labels for stepper
const initialLabels = ["First", "Second", "Third"]
const finalLabels = ["One from the End", "Final"]
const steps = initialLabels.concat(copiedLabels).concat(finalLabels)
//Set content for stepper
//moved into useEffect
// function getMiddleContent(){
// const content = copiedObjs.map((obj, idx) => (
// <StepDynamic key={idx} props={obj} />
// ))
// setMiddleContent(content)
// }
useEffect(() => {
const content = copiedObjs.map((obj, idx) => (
<StepDynamic key={idx} props={obj} />
))
setMiddleContent(content)
}, [middleContent, copiedObjs])
useEffect(() => {
//add/delete steps
function addStep(n = 1){
let newSteps = [...copiedLabels];
let newContent = [...middleContent];
let newLabel = ["new obj"]
newSteps.push(...newLabel);
console.log("midContent pre push: ", middleContent)
let content = [<StepDynamic key={Math.random()*3} props={null} />]
newContent.push(...content)
console.log("postPush, newContent: ", content)
setCopiedLabels(newSteps)
setMiddleContent(content)
}
function removeStep(idx){
let newSteps = [...copiedLabels];
let newContent = [...middleContent];
let steps = newSteps.filter((item, i) => i !== idx);
let content = newContent.filter((item, i) => i !== idx)
setCopiedLabels(steps)
setMiddleContent(content)
}
const initialContent = [<StepOne key={1} />, <StepTwo key={2} />, <StepThree key={3} addStep={addStep} removeStep={removeStep} titles={copiedLabels} />]
const finalContent = [<StepPenUltimate key={4} />, <StepFinal key={5} />]
const content = initialContent.concat(middleContent).concat(finalContent)
setStepsContent(content)
}, [middleContent, copiedLabels])
function getStepsContent(stepIndex) {
return stepsContent[stepIndex]
}
//Moved this section into useEffect to see if I got better results, but responds the same as before
// //add/delete steps
// function addStep(n = 1){
// let newSteps = [...copiedLabels];
// let newContent = [...middleContent];
// let newLabel = ["new obj"]
// newSteps.push(...newLabel);
// console.log("midContent pre push: ", middleContent)
// let content = [<StepDynamic key={Math.random()*3} props={null} />]
// newContent.push(...content)
// console.log("postPush, newContent: ", content)
// setCopiedLabels(newSteps)
// setMiddleContent(content)
// }
// function removeStep(idx){
// let newSteps = [...copiedLabels];
// let newContent = [...middleContent];
// let steps = newSteps.filter((item, i) => i !== idx);
// let content = newContent.filter((item, i) => i !== idx)
// setCopiedLabels(steps)
// setMiddleContent(content)
// }
return (
<>
<Button variant="contained" color="primary" onClick={handleClickOpen}>
New Stepper
</Button>
<Dialog open={open} onClose={handleClose}>
<DialogTitle>Stepper Guide</DialogTitle>
<DialogContent>
<DialogContentText>
Just some words of guidance
</DialogContentText>
<div>
<Stepper activeStep={activeStep} alternativeLabel>
{steps && steps.map((label) => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
))}
<br />
</Stepper>
<>
{activeStep === steps.length ? (
"Complete"
) : (
<>
{getStepsContent(activeStep)}
<Button color='warning' onClick={handleBack}>
{activeStep === steps[0] ? "" : "Back"}
</Button>
<Button color="primary" onClick={handleNext}>
{activeStep === steps.length ? "Submit" : "Next"}
</Button>
</>
)}
</>
</div>
<DialogActions>
<Button color="error" onClick={handleClose}>
Cancel
</Button>
</DialogActions>
</DialogContent>
</Dialog>
</>
)
}
And heres the CSB: https://codesandbox.io/s/eager-cerf-tsrbci?file=/src/Stepper.js:463-5442
UPDATE
After playing around and a lot of console.logs I think I've figured out where the issue is, but I'm not sure how to resolve it. When adding/ removing a step, the functions are wrapped within the same useEffect as the logic to set the content for the steps. This was done to be able to provide the functions to the StepThree component where the adding/ removing takes place on the UI. However, it seems the state update and the logic setting the content are not on the same time cycle and so the content setting does not have access to the new updated state. I've tried breaking apart the useEffect into multiple and tried just writing separate functions, but because there is a lot of conditional state being utilized, and in order for the different pieces to correctly have access, as well as to avoid infinite re-render loops, I keep coming back to the same setup. The label setting works fine because the setting of the array for the labels happens outside the useEffect that compiles the conditional state.
If I'm wrong on my assessment please let me know, and if you have any ideas on how to better structure this, I'd love to hear it. I've looked at useReducer as a possible alternative, but I'm not that familiar with the react hooks beyond useState and useEffect, and so far haven't determined how to write out the reducer, again I keep coming back to needing to have the conditional pieces of state available at the right place in the code.

React state updating without setstate, takes on state of deleted item (SOLVED)

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!

How to filter array to match params value with react

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

Reactj.s: Item deleted only after refresh | Delete Method

I'm trying to send a delete request to delete an item from an API.
The API request is fine when clicking on the button. But Item get's deleted only after refreshing the browser!
I'm not too sure if I should add any parameter to SetHamsterDeleted for it to work?
This is what my code looks like.
import React, {useState} from "react";
const Hamster = (props) => {
const [hamsterDeleted, setHamsterDeleted] = useState("")
async function deleteHamster(id) {
const response = await fetch(`/hamsters/${id}`, { method: "DELETE" });
setHamsterDeleted()
}
return (
<div>
<p className={props.hamster ? "" : "hide"}>
{hamsterDeleted}
</p>
<button onClick={() => deleteHamster(props.hamster.id)}>Delete</button>
<h2>{props.hamster.name}</h2>
<p>Ålder:{props.hamster.age}</p>
<p>Favorit mat:{props.hamster.favFood}</p>
<p>Matcher:{props.hamster.games}</p>
<img src={'./img/' + props.hamster.imgName} alt="hamster"/>
</div>
)
};
export default Hamster;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
Imagine you have a parent component (say HamstersList) that returns/renders list of these Hamster components - it would be preferable to declare that deleteHamster method in it, so it could either: a) pass some prop like hidden into every Hamster or b) refetch list of all Hamsters from the API after one got "deleted" c) remove "deleted" hamster from an array that was stored locally in that parent List component.
But since you are trying to archive this inside of Hamster itself, few changes might help you:
change state line to const [hamsterDeleted, setHamsterDeleted] = useState(false)
call setHamsterDeleted(true) inside of deleteHamster method after awaited fetch.
a small tweak of "conditional rendering" inside of return, to actually render nothing when current Hamster has hamsterDeleted set to true:
return hamsterDeleted ? null : (<div>*all your hamster's content here*</div>)
What do you want to do in the case the hamster is deleted? If you don't want to return anything, you can just return null.
I'm not too sure if I should add any parameter to SetHamsterDeleted for it to work?
Yes, I'd make this a boolean instead. Here's an example:
import React, { useState } from "react";
const Hamster = (props) => {
const [hamsterDeleted, setHamsterDeleted] = useState(false);
async function deleteHamster(id) {
const response = await fetch(`/hamsters/${id}`, { method: "DELETE" });
setHamsterDeleted(true);
}
if (hamsterDeleted) return null;
return (
<div>
<p className={props.hamster ? "" : "hide"}>
{hamsterDeleted}
</p>
<button onClick={() => deleteHamster(props.hamster.id)}>Delete</button>
<h2>{props.hamster.name}</h2>
<p>Ålder:{props.hamster.age}</p>
<p>Favorit mat:{props.hamster.favFood}</p>
<p>Matcher:{props.hamster.games}</p>
<img src={'./img/' + props.hamster.imgName} alt="hamster"/>
</div>
);
};
HOWEVER! Having each individual hamster keep track of its deleted state doesn't sound right (of course I don't know all your requirements but it seems odd). I'm guessing that you've got a parent component which is fetching all the hamsters - that would be a better place to keep track of what has been deleted, and what hasn't. That way, if the hamster is deleted, you could just not render that hamster. Something more like this:
const Hamsters = () => {
const [hamsers, setHamsters] = useState([]);
// Load the hamsters when the component loads
useEffect(() => {
const loadHamsters = async () => {
const { data } = await fetch(`/hamsters`, { method: "GET" });
setHamsters(data);
}
loadHamsters();
}, []);
// Shared handler to delete a hamster
const handleDelete = async (id) => {
await fetch(`/hamsters/${id}`, { method: "DELETE" });
setHamsters(prev => prev.filter(h => h.id !== id));
}
return (
<>
{hamsters.map(hamster => (
<Hamster
key={hamster.id}
hamster={hamster}
onDelete={handleDelete}
/>
))}
</>
);
}
Now you can just make the Hamster component a presentational component that only cares about rendering a hamster, eg:
const Hamster = ({ hamster, onDelete }) => {
const handleDelete = () => onDelete(hamster.id);
return (
<div>
<button onClick={handleDelete}>Delete</button>
<h2>{hamster.name}</h2>
<p>Ålder:{hamster.age}</p>
<p>Favorit mat:{hamster.favFood}</p>
<p>Matcher:{hamster.games}</p>
<img src={'./img/' + hamster.imgName} alt="hamster"/>
</div>
);
};

useState value changes when i call it from child

I'm creating a timer app built using react-hooks and an array of this timers
I don't understand why timerList changes
Here it is the parent component
const [timerList, setTimerList] = useState([]);
const removeTimer = () => {
console.log("timerList", timerList);
};
return (
<div id="main">
{timerList ? timerList.map((child) => child) : null}
<div className="add-button before">
<button
onClick={() => {
const time = new Date();
time.setSeconds(time.getSeconds() + 0);
setTimerList((timerList) => [
...timerList,
<FullTimer
expiryTimestamp={time}
removeTimer={() => {
removeTimer();
}}
id={window.prompt("Insert timer name") + ` ${timerList.length}`}
key={timerList.length}
/>,
]);
}}
>
The interested child's component part:
<button
onClick={() => {
removeTimer();
}}
>
The child component is a custom timer with some css, and when i call removeTimer the value of timerList (in the parent component) changes, when it should remain the same.
What am I missing?
P.S. the button tags aren't closed because i have some element inside them that use awesome-font
Side note: In general it's considered bad practice to store components in another components state.
But that's not really the problem here. Given your code, it's a simple closure problem.
This:
const removeTimer = () => {
console.log("timerList", timerList);
};
definition closes over the current timerList. So it will log it, as it was when removeTimer was assigned. Currently that's on every render. So it will log the state seemingly one step behind. There's no fix around that, because that's just how closures work.
Provided you actually want to remove a timer, when removeTimer is invoked, you would need to use the callback version of the updater (setTimerList) and pass some identifying value so that you can actually remove the correct one.
This would all be a lot simpler, if you followed the initial advice and don't store the component in the state, but rather it's defining properties.
The following would be a working example (please excuse my typescript):
import React, { useState } from 'react';
type FullTimerProps = {
id: string;
expiryTimestamp: Date;
removeTimer: () => void;
}
const FullTimer = ({expiryTimestamp, removeTimer, id}: FullTimerProps): JSX.Element => {
return (
<div>
<button onClick={removeTimer}>remove</button>
{id}: {expiryTimestamp.toLocaleDateString()}
</div>
);
};
type Timer = {
id: string;
expiryTimestamp: Date;
};
const TimerList = (): JSX.Element => {
const [timerList, setTimerList] = useState<Timer[]>([]);
const removeTimer = (timer: Timer) => {
setTimerList(timerList => timerList.filter(t => t.id !== timer.id));
};
return (
<div id="main">
{timerList.map(timer => (
<FullTimer
key={timer.id}
id={timer.id}
expiryTimestamp={timer.expiryTimestamp}
removeTimer={() => removeTimer(timer)}
/>
))}
<div className="add-button before">
<button
onClick={() =>
setTimerList(timerList => [...timerList, {
id: window.prompt('Insert timer name') + ` ${timerList.length}`,
expiryTimestamp: new Date()
}])}
>Add
</button>
</div>
</div>
);
};
changing this code snippet
setTimerList((timerList) => [
...timerList,
<FullTimer
expiryTimestamp={time}
removeTimer={() => removeTimer()}
id={window.prompt("Insert timer name") + ` ${timerList.length}`}
key={timerList.length}
/>,
]);
to
timerList.push(<FullTimer
expiryTimestamp={time}
removeTimer={() => removeTimer()}
id={window.prompt("Insert timer name") + ` ${timerList.length}`}
key={timerList.length}
/>);
setTimerList([...timerList]);
Fixed the problem you are having. Although this change is not recommended because it is not immutable approach, but it fixes this case.
UPDATE: It turned out that you duplicated the removeTimer function during the setTimerList call which cause the child component to capture the timerList at the moment of assignment. Which is mentioned at https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures?retiredLocale=vi#closure as mr #yoshi has shown
Try to write your onclick function like this
<button
onClick={() => removeTimer()}
>
Also over here
<FullTimer
expiryTimestamp={time}
removeTimer={() => removeTimer()}

Categories