I am totally confused about this scenario , I am having a state variable called listItems setting the value for listItems using the api call inside useEffect now in the handleChange I am changing the particular object value inside the listItems but I didn't change the actual listItems value but if i console the listItems it's showing as updated value even without setList how come it happens?
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import OrderSummary from './orderSummary'
export default function displayItems() {
const [listItems, setList] = useState([]);
const [order, setorder] = useState([]);
var newarr = [];
useEffect(() => {
axios.post('http://localhost:3006/listItem', {
})
.then(function (resp) {
let res = resp.data.sendList.response;
let newRes = res.map((item) => {
return item;
})
setList(newRes);
})
.catch(function (error) {
console.log(error);
});
}, [])
function handleChange(type,item) {
var arrList=item;
var newAr=[];
if (type === 1) {
arrList.quantity=arrList.quantity+1;
}
else if (type === 0 && item.quantity > 1) {
arrList.quantity=arrList.quantity-1;
}
newAr.push(arrList);
console.log("test",listItems) // value changes here dont know how
// setList(listItems);
}
function placeOrder(item) {
newarr.push(...order, item);
setorder(newarr)
}
return (
<div className="col">
<div className="row">
<div classname="col-md-6">
<p><b>Available Items</b> </p>
{listItems && listItems.map((item) => {
return (
<div key={item._id}>
<p>Name:{item.name}</p>
<p>Description:{item.description}</p>
<p>Cost:{'₹'}{' '}{item.cost}</p>
<p>Quantity:{' '}
<i onClick={() => handleChange(1,item)} className="fa fa-plus-circle" />
<span className="text-center"><b>{item.quantity}</b></span><i onClick={() => handleChange(0,item)} className="fa fa-minus-circle" /></p>
<div>
<button onClick={() => placeOrder(item)}>Add to order</button>
</div>
</div>)
})}
</div>
{order && <OrderSummary orderItems={order} />}
</div>
</div>
)
}
sandox
The following code var arrList=item; is an assignment by reference, it means that arrList and item are both references to the same object which explains the modification of the second when modifying the first, if you want to clone an object you can use Object.assign() or the Spread operator or another solution:
var arrList = Object.assign({}, item);
// Or
var arrList = {...item};
Working demo:
Related
I have a contentEditable component:
EditableComponent.js
const EditableComponent = (props) => {
return <p contentEditable>{props.children}</p>;
};
In the ParentComponent I can add EditableComponents to an array (someArr) with useState, and then I pass someArr and setSomeArray via props to another component (AllEditable) to render it:
ParentComponent.js
import EditableComponent from "./components";
import AllEditable from "./components";
const ParentComponent = () => {
const [someArr, setSomeArr] = useState([]);
const handleNewEditable = () => {
setContentArr((prevContentArr) => {
return [...prevContentArr, <EditableComponent />];
});
};
return (
<div className="wrapper">
<AllEditable someArr={someArr} setSomeArr={setSomeArr} />
<div>
<button onClick={handleNewEditable}>Add</button>
</div>
</div>
);
};
Each rendered component (EditableComponent) have a span with the content 'X' that should delete the target onClick:
AllEditable.js
const AllEditable= (props) => {
const deleteContentHandler = (index) => {
props.setSomeArr((prevState) => {
return prevState.filter((_, idx) => idx !== index);
});
};
return (
<div>
{props.someArr.map((content, idx) => {
return (
<div key={`content-${idx}`}>
<span onClick={() => {deleteContentHandler(idx);}}>
X
</span>
<div>{content}</div>
</div>
);
})}
</div>
);
};
The problem:
It doesn't matter which component I'm trying to delete, it removes the last component (even in the Components section in the developer tools) and I'm pretty sure that the logic behind deleting (filter) works well.
I tried deleting the contentEditable attribute, and added some unique random text in each component and then it worked as expected!.
Things I tried
Creating a new array without the removed target
Nesting the components in someArr in objects with key: index, example: {idx: 0, content: <EditableComponent />}
Added a function - onDoubleClick for each EditableComponent to toggle the attribute contentEditable, true or false.
Replaced the element in EditableComponent to <textarea></textarea> instead of <p contentEditable></p>
Your problem is all your EditableComponent components have the same key (because you haven't declared key on them). React cannot identify which EditableComponent you want to delete. You can check this document.
I'd suggest you add a key attribute like below
<EditableComponent key={prevContentArr.length - 1}/>
For safer index reservation, you should use map instead filter
const AllEditable= (props) => {
const deleteContentHandler = (index) => {
props.setSomeArr((prevState) => {
return prevState.map((x, idx) => idx !== index ? x : null);
});
};
return (
<div>
{props.someArr.map((content, idx) => {
return content ? (
<div key={`content-${idx}`}>
<span onClick={() => {deleteContentHandler(idx);}}>
X
</span>
<div>{content}</div>
</div>
) : null;
})}
</div>
);
};
const { useState } = React
const EditableComponent = (props) => {
return <p contentEditable>{props.children}</p>;
};
const AllEditable= (props) => {
const deleteContentHandler = (index) => {
props.setSomeArr((prevState) => {
return prevState.map((x, idx) => idx !== index ? x : null);
});
};
return (
<div>
{props.someArr.map((content, idx) => {
return content ? (
<div key={`content-${idx}`}>
<span onClick={() => {deleteContentHandler(idx);}}>
X
</span>
<div>{content}</div>
</div>
) : null;
})}
</div>
);
};
const ParentComponent = () => {
const [someArr, setSomeArr] = useState([]);
const handleNewEditable = () => {
setSomeArr((prevContentArr) => {
return [...prevContentArr, <EditableComponent key={prevContentArr.length - 1}/>];
});
};
return (
<div className="wrapper">
<AllEditable someArr={someArr} setSomeArr={setSomeArr} />
<div>
<button onClick={handleNewEditable}>Add</button>
</div>
</div>
);
};
ReactDOM.render(
<ParentComponent/>,
document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Side note that keys with index values are not the best because your array keeps changing that would make key changes as well. You can use some unique key generator to handle that situation instead.
You shluld change the way you set keys, setting the element key to: "content-index" is confusing for react, because once you remove an item all the indexes will change, and therefore your keys.
So you need to find a way to have static keys for your elements. That way everytime they render, the key will be the same.
The logic is working correctly and in fact it is deleting the correct elements, however since you are using the index to identify elements, you will never see this since it will always appear that only the last one is removed (when the array updates, the indices will update too).
So if you had 0,1,2 and removed 1 now you have an array with indices 0,1
To test this you can place a reference to the index when rendering the content editable div, based on testing you can see that the correct element is in fact being removed:
import "./styles.css";
import React, { useState } from "react";
const EditableComponent = (props) => {
return (
<p contentEditable>
{props.children}
{props.idtfy}
</p>
);
};
const AllEditable = (props) => {
const deleteContentHandler = (index) => {
props.setSomeArr((prevState) => {
return prevState.filter((_, idx) => idx !== index);
});
};
return (
<div>
{props.someArr.map((content, idx) => {
return (
<div key={`content-${idx}`}>
<span>{idx}</span>
<span
onClick={() => {
deleteContentHandler(idx);
}}
>
X
</span>
<div>{content}</div>
</div>
);
})}
</div>
);
};
const ParentComponent = () => {
const [someArr, setSomeArr] = useState([]);
const handleNewEditable = () => {
setSomeArr((prevContentArr) => {
return [
...prevContentArr,
<EditableComponent idtfy={prevContentArr.length + 1} />
];
});
};
return (
<div className="wrapper">
<AllEditable someArr={someArr} setSomeArr={setSomeArr} />
<div>
<button onClick={handleNewEditable}>Add</button>
</div>
</div>
);
};
That said, your key should probably point to some static value, but that will only impact rendering order, not which item is being clicked/closed.
I am trying to capitalize the product name after fetching if from a products list using a button.
Basically, a button fetches the data. Which gives us the product name as a heading with a CLEAR and CAPITALIZE button with each item.
Clear button removes the item from the list. Implemented.
Capitalize button Capitalizes the product name.
Not able to figure out how to implement the capitalize function.
CODE::
capHandler function implements the functionality.
import React, { useRef, useState } from 'react';
import axios from 'axios';
function App(){
const url = 'https://course-api.com/react-store-products';
const [products, setProducts] = useState([]);
const productNameRef = useRef();
const fetchData = async () => {
const { data } = await axios.get(url);
setProducts(data);
};
const clearProducts = () => {
setProducts([]);
};
const clearSingleProduct = (productID) => {
const filteredArray = products.filter((item) => item.id != productID);
setProducts(filteredArray);
};
function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
function capHandler() {
console.log('Single Word Capitalized');
}
return (
<>
<button onClick={fetchData}>Fetch Products</button>
<button onClick={clearProducts}>Clear Products</button>
<div>
{products.map((product) => {
return (
<div ref={productNameRef} id={product.id} key={product.id}>
<h3 className="productName">{product.name}</h3>
<button onClick={() => clearSingleProduct(product.id)}>
Clear
</button>
<button onClick={capHandler}>Capitalize</button>
</div>
);
})}
</div>
</>
);
};
export default App;
you make the capHandler accept product index id and update the state.
function capHandler(id) {
const newProducts = [...products]
newProducts[i].name = capitalizeFirstLetter(newProducts[i].name)
setProducts([...newProducts]);
}
Also need to pass the map index while mapping the products
<div>
{products.map((product, i) => {
return (
...
Lastly call the fucntion in the button
<button onClick={() => capHandler(i)}>Capitalize</button>
Update the capHandler() function with the following body:
function capHandler() {
setProducts(products.map(i => ({...i , name: capitalizeFirstLetter(i.name)}))
}
Below, i am rendering <App/> component with children as <Input/> component array. I added few inputs using "add new" button. I am able to add input text components. But, when i am typing value in text, it is not displaying. i am not able to modify object in state array since index is showing as "-1" in setData function. Due to this, value is not showing when we type in text box. Please let me know why state is [] when i am accessing in setData function.
function Input(props)
{
return (
<div>
<label htmlFor='variable'>Name</label>
<input id='variable'
type='text'
value={props.value}
onChange={(e) => props.setData(props.id, e.target.value)} />
</div>
)
}
function App()
{
let [state, setState] = React.useState([])
let [inputs, setInputs] = React.useState([])
let setData = ((id, value) =>
{
console.log(state); // prints []
let index = state.findIndex(ele => ele.key === id);
console.log(index); // prints -1
if (!(index === -1))
{
setState(state =>
{
state[idx]["value"] = value;
})
}
})
let handleAdd = () =>
{
let idx = `${new Date().getTime()}`
let tempState = {
"key": idx,
"value": "",
}
setState(state => [...state, tempState])
let input = <Input key={tempState.key}
value={tempState.value}
id={tempState.key}
setData={setData} />
setInputs(inputs => [...inputs, input])
}
return (
<div>
<button onClick={handleAdd}>add new</button>
<div>
{inputs}
</div>
</div>
)
}
When you create an Input component inside handleAdd, it creates a closure and as a result setData gets the state that existed when the component was created, missing the newly added state.
In general, creating components and saving them to state is not a good approach. Instead it's better to only save the data onto state and render the components based on it.
Here's one way to do this, note how much simpler the component and its logic are.
function App() {
let [state, setState] = React.useState([]);
let setData = (id, value) => {
const newState = state.map((st) => {
if (st.key === id) {
st.value = value;
}
return st;
});
setState(newState);
};
const addInput = () => {
const idx = `${new Date().getTime()}`;
setState([...state, { key: idx, value: '' }]);
};
return (
<div>
<button onClick={addInput}>add new</button>
<div>
{state.map((st) => (
<Input value={st.value} key={st.key} setData={setData} id={st.key} />
))}
</div>
</div>
);
}
Datalist is an array I'm trying to concat the boards array with the Datalist array, but when I console it doesn't reflect. On the other hand when I assign Datalist.concat(boards) to a variable it reflects example
const newArr = Datalist.concat(boards);
console.log(newArr)
(main code) please help me review it. Thanks in advance
import React, { useState, useEffect } from 'react';
import Modal from './Modal';
import { Datalist } from '../Data/Boards';
function Boards() {
const [boards, setboards] = useState(JSON.parse(localStorage.getItem('boards')) || []);
const [title, settitle] = useState('');
localStorage.setItem('boards', JSON.stringify(boards));
Datalist.concat(boards);
console.log(Datalist);
const handleChange = (e) => {
settitle(e.target.value);
};
const handleSubmit = () => {
if (title.length === 0) {
return;
}
setboards((prev) => [...prev, title]);
};
return (
<div>
<ul id="boards">
<BoardList boards={boards} />
</ul>
<Modal title={title} handleChange={handleChange} handleSubmit={handleSubmit} />
</div>
);
}
function BoardList({ boards }) {
const history = useHistory();
return (
<>
{boards.map((board, index) => (
<li
key={index}
onClick={() => {
history.push('./workspace');
}}
>
<h3>{board}</h3>
</li>
))}
</>
);
}
export default Boards;
That is the expected behaviour. The concat function does not alter the original arrays. You can read about it in the MDN docs
For your case you should be able to do Datalist = Datalist.concat(boards); and it should work like you're expecting
Sorry for the confusing question. First I will like to share my code:
const intialState = {
output: null,
bookIdToDelete: null
}
const [state,setState] = useState(intialState)
useEffect(() => {
getDataFromServer().then(data => {
console.log(data);
if (data != 2) {
// job gonna be here
const output = data.map(book => {
return (
<div key={book._id} className="col-md-3">
<div className="item">
<img className="bookimage" src={book.imgs[0]} alt="img"/>
<h3>
<Link to={"/admin/mybook/" + book._id}>{book.title}</Link>
</h3>
<h6>
<Link to={"/admin/mybook/" + book._id}>Edit</Link>
<button id={book._id} onClick={deleteBtnClick} className="btn btn-danger">Delete</button>
</h6>
</div>
</div>
)
})
setState({...state, output})
}
}).catch(error => {
console.log(error);
})
}, [])
const deleteBtnClick = (e) => {
let bookId = e.target.id;
console.log(state)
setState({
...state,
bookIdToDelete: bookId
})
}
console.log('showmodal',state);
return (
<>
{state.output}
</>
)
}
export default MyBooks
Basically what this does is that I obtain an array with some information and create a list using a map. I do this all inside the useEffect. Every item has a delete button that calls another function that just saves the id of the element that I want to delete inside the state. What is happening is that when I click on this delete button the state is always equal to the initial state and not with the one I updated in the useEffect. It seems that as I created the array of elements inside the useEffect before I set the state to the its new value the state that is used in the "deleteBtnClick" function is the old one, in this case the initial one....
So I solved this by just saving the information in the useEffect and then creating the list with the map before the return of the component... But I still have the doubt of why it was happening... so if you know why I will appreciate the explanation. Thanks!
in general I would organize my code like this:
const intialState = {
output: [],
bookIdToDelete: null
}
const [state,setState] = useState(intialState)
useEffect(() => {
getDataFromServer().then(data => {
console.log(data);
if (data != 2) {
// job gonna be here
const output = data
setState({...state, output})
}
}).catch(error => {
console.log(error);
})
}, [])
const deleteBtnClick = (e) => {
let bookId = e.target.id;
console.log(state)
setState({
...state,
bookIdToDelete: bookId
})
}
return (
<>
{state.output.map(book => {
return (
<div key={book._id} className="col-md-3">
<div className="item">
<img className="bookimage" src={book.imgs[0]} alt="img"/>
<h3>
<Link to={"/admin/mybook/" + book._id}>{book.title}</Link>
</h3>
<h6>
<Link to={"/admin/mybook/" + book._id}>Edit</Link>
<button id={book._id} onClick={deleteBtnClick} className="btn btn- danger">Delete</button>
</h6>
</div>
</div>
)
})}
</>
)
}
export default MyBooks