I am trying to update state on click event using react hooks. State changes, but component doesn't rerender. Here is my code snippet:
function ThirdPage() {
const [selectedIngredients, setSelectedIngredients] = useState([])
const DeleteIngredient = (ingredient) => {
let selectedIngredientsContainer = selectedIngredients;
selectedIngredientsContainer.splice(selectedIngredientsContainer.indexOf(ingredient), 1);
setSelectedIngredients(selectedIngredientsContainer);
console.log(selectedIngredients);
}
const selectedIngredientsDiv = selectedIngredients.map(ingredient =>
(
<div className={styles.selectedIngredientsDiv}>{ingredient}
<div className={styles.DeleteIngredient}
onClick={() => {
DeleteIngredient(ingredient)}}>x</div></div>
))
return (
...
What am I doing wrong? Thanks in advance!
Issue with you splice as its not being saved to selectedIngredientsContainer. I would do following:
selectedIngredientsContainer = selectedIngredientsContainer.filter(value => value !== ingredient);
or
selectedIngredientsContainer.splice(selectedIngredientsContainer.indexOf(ingredient), 1 );
setSelectedIngredients([...selectedIngredientsContainer]);
Hope it helps.
normally I would leave an explanation on what's going on but tldr is that you should check first to make sure that you're array isn't empty, then you you can filter out the currentIngredients. Also you don't need curly brackets to call that function in the jsx but that can be personal flavor for personal code. I apologize if this doesn't help but I have to head out to work. Good luck!
function ThirdPage() {
const [selectedIngredients, setSelectedIngredients] = useState([]);
const DeleteIngredient = ingredient => {
// let selectedIngredientsContainer = selectedIngredients;
// selectedIngredientsContainer.splice(selectedIngredientsContainer.indexOf(ingredient), 1);
// setSelectedIngredients(selectedIngredientsContainer);
// console.log(selectedIngredients);
if (selectedIngredients.length > 0) {
// this assumes that there is an id property but you could compare whatever you want in the Array.filter() methods
const filteredIngredients = setSelectedIngredients.filter(selectedIngredient => selectedIngredient.id !== ingredient.id);
setSelectedIngredients(filteredIngredients);
}
// nothing in ingredients - default logic so whatever you want
// log something for your sanity so you know the array is empty
return;
};
const selectedIngredientsDiv = selectedIngredients.map(ingredient => (
<div className={styles.selectedIngredientsDiv}>
{ingredient}
<div className={styles.DeleteIngredient} onClick={() => DeleteIngredient(ingredient)}>
x
</div>
</div>
));
}
The answer is very Simple, your state array selectedIngredients is initialized with an empty array, so when you call map on the empty array, it will not even run once and thus DeleteIngredient is never called and your state does not change, thus no re-render happens
Related
I have a dropdown list that contains some cart items from a shop. I want the dropdown to re-render every time a cart item is added, but it doesn't and only shows my new cart addition when I close and open the cart again (remounts).
const CartDropdown = () => {
const {setCartProducts, cartProducts} = useContext(CartContext)
const {setProducts, currentProducts} = useContext(ProductsContext)
// useEffect(() => {}, [cartProducts])
const cleanCart = () => {
const cleanProducts = currentProducts
console.log(cleanProducts)
for (let i in cleanProducts) {
if (cleanProducts[i].hasOwnProperty('quantity')){
cleanProducts[i].quantity = 0
}
}
setProducts(cleanProducts)
setCartProducts([])
}
return(
<div className='cart-dropdown-container'>
<div className='cart-items' forceRemount={force}>
{cartProducts.map((product) => (
<div>
<img src={product.imageUrl}></img>
</div>)
)}
</div>
<button onClick={cleanCart}>LIMPAR</button>
<Button children={'FINALIZE PURCHASE'}/>
</div>
)
}
I want the CartDropdown to remount when the cartProducts changes.
It really depends on what those setters and getters on your useContext are returning.
Assuming they are from a useState(), then you have to make sure you always pass a different object to the setters.
From the docs:
Bailing out of a state update
If you update a State Hook to the same value as the current state,
React will bail out without rendering the children or firing effects.
(React uses the Object.is comparison algorithm.)
In other words, simply mutating currentProducts and calling setProducts passing the same (but now mutated) object will not trigger a reprender.
So your code
const cleanProducts = currentProducts
console.log(cleanProducts)
// Mutating cleanProducts
setProducts(cleanProducts)
Should be something like
const cleanProducts = [...currentProducts] // <--- changed this line
console.log(cleanProducts)
// Mutating cleanProducts
setProducts(cleanProducts)
And you have to do this everywhere you call setters.
Another thing, you should add a key attribute to elements used in a .map(). For instance:
{cartProducts.map((product) => (
<div key={product.imageUrl}>
<img src={product.imageUrl}></img>
</div>)
)}
I am trying to find an item from a collection, from the code below, in order to update my react component, the propertState object isnt empty, it contains a list which i have console logged, however I seem to get an underfined object when i console log the value returned from my findProperty function... I am trying update my localState with that value so that my component can render the right data.
const PropertyComponent = () => {
const { propertyId } = useParams();
const propertyState: IPropertiesState = useSelector(
propertiesStateSelector
);
const[property, setProperty] = useState()
const findProperty = (propertyId, properties) => {
let propertyReturn;
for (var i=0; i < properties.length; i++) {
if (properties[i].propertyId === propertyId) {
propertyToReturn = properties[i];
break;
}
}
setProperty(propertyReturn)
return propertyReturn;
}
const foundProperty = findProperty(propertyId, propertyState.properties);
return (<>{property.propertyName}</>)
}
export default PropertyComponent
There are a few things that you shall consider when you are finding data and updating states based on external sources of data --useParams--
I will try to explain the solution by dividing your code in small pieces
const PropertyComponent = () => {
const { propertyId } = useParams();
Piece A: Consider that useParams is a hook connected to the router, that means that you component might be reactive and will change every time that a param changes in the URL. Your param might be undefined or an string depending if the param is present in your URL
const propertyState: IPropertiesState = useSelector(
propertiesStateSelector
);
Piece B: useSelector is other property that will make your component reactive to changes related to that selector. Your selector might return undefined or something based on your selection logic.
const[property, setProperty] = useState()
Piece C: Your state that starts as undefined in the first render.
So far we have just discovered 3 pieces of code that might start as undefined or not.
const findProperty = (propertyId, properties) => {
let propertyReturn;
for (var i=0; i < properties.length; i++) {
if (properties[i].propertyId === propertyId) {
propertyToReturn = properties[i];
break;
}
}
setProperty(propertyReturn)
return propertyReturn;
}
const foundProperty = findProperty(propertyId, propertyState.properties);
Piece D: Here is where more problems start appearing, you are telling your code that in every render a function findProperty will be created and inside of it you are calling the setter of your state --setProperty--, generating an internal dependency.
I would suggest to think about the actions that you want to do in simple steps and then you can understand where each piece of code belongs to where.
Let's subdivide this last piece of code --Piece D-- but in steps, you want to:
Find something.
The find should happen if you have an array where to find and a property.
With the result I want to notify my component that something was found.
Step 1 and 2 can happen in a function defined outside of your component:
const findProperty = (propertyId, properties) => properties.find((property) => property.propertyId === propertyId)
NOTE: I took the liberty of modify your code by simplifying a little
bit your find function.
Now we need to do the most important step, make your component react at the right time
const findProperty = (propertyId, properties) => properties.find((property) => property.propertyId === propertyId)
const PropertyComponent = () => {
const { propertyId } = useParams();
const propertyState: IPropertiesState = useSelector(
propertiesStateSelector
);
const[property, setProperty] = useState({ propertyName: '' }); // I suggest to add default values to have more predictable returns in your component
/**
* Here is where the magic begins and we try to mix all of our values in a consistent way (thinking on the previous pieces and the potential "undefined" values) We need to tell react "do something when the data is ready", for that reason we will use an effect
*/
useEffect(() => {
// This effect will run every time that the dependencies --second argument-- changes, then you react afterwards.
if(propertyId, propertyState.properties) {
const propertyFound = findProperty(propertyId, propertyState.properties);
if(propertyFound){ // Only if we have a result we will update our state.
setProperty(propertyFound);
}
}
}, [propertyId, propertyState.properties])
return (<>{property.propertyName}</>)
}
export default PropertyComponent
I think that in this way your intention might be more direct, but for sure there are other ways to do this. Depending of your intentions your code should be different, for instance I have a question:
What is it the purpose of this component? If its just for getting the property you could do a derived state, a little bit more complex selector. E.G.
function propertySelectorById(id) {
return function(store) {
const allProperties = propertiesStateSelector(store);
const foundProperty = findProperty(id, allProperties);
if( foundProperty ) {
return foundProperty;
} else {
return null; // Or empty object, up to you
}
}
}
Then you can use it in any component that uses the useParam, or just create a simple hook. E.G.
function usePropertySelectorHook() {
const { propertyId } = useParams();
const property = useSelector(propertySelectorById(propertyId));
return property;
}
And afterwards you can use this in any component
functon AnyComponent() {
const property = usePropertySelectorHook();
return <div> Magic {property}</div>
}
NOTE: I didn't test all the code, I wrote it directly in the comment but I think that should work.
Like this I think that there are even more ways to solve this, but its enough for now, hope that this helped you.
do you try this:
const found = propertyState.properties.find(element => element.propertyId === propertyId);
setProperty(found);
instead of all function findProperty
Hi i am new ish to JavaScript/React and I am currently making a project to practice it more.
I have an expenses list with some expenses all with a unique Id stored as props.items but i'm trying to add a delete button so that an expense will be removed from props.items when its clicked. Is there a way i can remove an item from props.items with the use of the unique ID?
Currently I have this where idNumber is the unique id sent back from the child component ExpenseItem
const onRemoveExpense = (idNumber) => {
console.log("remove clicked", idNumber)
console.log(props.items, "<- all items")
}
return (
<ul className='expenses-list'>
{props.items.map((expense) => (
<ExpenseItem
key={expense.id}
value={expense.id}
title={expense.title}
amount={expense.amount}
date={expense.date}
removeExpense={onRemoveExpense}
/>
))}
</ul>
);
}
Thanks for the help!
The biggest hurdle I see here is that your items array is not in the state of the component in question-- it is passed in as props. So you'd want to define your deletion script in which component is holding the items in its component state. You'd write it somewhere along the lines of:
const onRemoveExpense = (idNumber) => {
this.setState((state, props) => {
// get items from current state
const { items } = state;
// get a new array with only those items that do *not* have the id number passed
const newItems = items.filter((item) => item.id !== idNumber);
// return it to set the new state
return newItems;
});
}
This would obviously need to be adjusted to your specific state and component structure. You'd then pass this as a prop along with the items to the component in question, and call it to trigger a deletion.
For a "hide" function instead of a delete one, you could try adding a shown boolean prop and then change that on click.
But to actually delete it, you'll need to have your items stored in state.
You could try something like this:
const [items, setItems] = useState(props.items)
// set the initial state as `props.items`
// (I'm assuming the code snippet you shared exists inside a functional component)
const onRemoveExpense = (idNumber) => {
console.log("remove clicked", idNumber)
console.log(props.items, "<- all items")
const newItems = items.filter(({ id }) => id !== idToDelete)
setItems(newItems)
}
return (
<ul className='expenses-list'>
{items.map((expense) => (
<ExpenseItem
key={expense.id}
value={expense.id}
title={expense.title}
amount={expense.amount}
date={expense.date}
removeExpense={() => onRemoveExpense(expense.id)}
/>
))}
</ul>
);
}
I might be forgetting something though—I haven't tested the above code. You might need to have a useEffect() to make it re-render properly when the state changes.
Or you can manage the state in the component that is defining items for this component.
so this function updates this state array:
let [Produits, setProduit] = useState(JSON.parse(Devis.produits))
let changeQte = (id, e) => {
let produittable = Produits;
produittable.forEach((p) => {
if (p.id == id) {
p.quantityAchete = parseInt(e.target.value);
}
});
setProduit(produittable);
};
the array did update without any problem but the changes aren't getting re-rendered
{console.log('rendering') ,Produits.map((p) => (
<div key={p.id} className="product_column flex_center">
<div className="productItem">{p.nom}</div>
<div className="productItem">{p.category}</div>
<div className="productItem">{p.prix_vente} DA</div>
<input
onChange={(e) => changeQte(p.id, e)}
type="number"
name="qte"
value={p.quantityAchete}
/>
as you can see i'm loggin to the console to check if that line is getting executed and it does ! but the values rendered doesn't update !
Don't mutate state, do this instead:
let changeQte = (id, e) => {
setProduit(existing => existing.map(c => c.id === id ? {...c,quantityAchete: parseInt(e.target.value)} : c))
};
These lines:
// this line just sets produittable to the same reference as Produits.
// so now produittable === Produits, it's essentially useless
let produittable = Produits;
produittable.forEach((p) => {
if (p.id == id) {
// you are mutating
p.quantityAchete = parseInt(e.target.value);
}
});
// because produittable === Produits, this doesn't do anything
setProduit(produittable);
In addition to what Adam said, besides not modifying the state directly, the reason you're not seeing any changes is because the component only gets rerendered when the state actually changed. And to know whether the state changed, react makes a shallow comparison between the two states. Since you modified the state directly, the reference remains the same and as such your component isn't rerendering.
To expand on Adam's answer, you can also clone the current state Produits and assign it to the local variable produittable and have the state update be recognized.
So instead of this:
let produittable = Produits;
You could simply clone it like so, with the spread operator:
let produittable = [...Produits];
I have code where if a function is invoked it will call toggleCheckedUser and pass along information about which object property to toggle. Then saves the modified object back to state (selectedSendTo).
However, when I run this, the toggle it works, but when I try to edit a second property, before changing it I try console.log(selectedSendTo) I always get the initial value whether it be an empty object {} or false instead of the previously updated object.
When I use useEffect to spy on selectedSendTo I can see that the setSelectedSendTo() function correctly updated the object. So for some reason when I revisit the object it's empty.
const [selectedSendTo, setSelectedSendTo] = useState(false);
const toggleCheckedUser = (companyID, contactID) => {
console.log(companyID, contactID);
console.log(selectedSendTo); // THIS VALUE IS always the same as INITIAL value
console.log(selectedSendTo[companyID]);
if(selectedSendTo[companyID] &&
selectedSendTo[companyID][contactID] === true){
//remove it
delete(selectedSendTo[companyID][contactID]);
}else{
setSelectedSendTo({
...selectedSendTo,
[companyID]:{
...selectedSendTo[companyID],
[contactID]: true,
}
})
}
}
Here is the DOM:
<CustomCheckbox
className="person__checkbox" name={`checkbox-${contactID}`}
alreadySelected={
selectedSendTo[company.companyID] &&
selectedSendTo[company.companyID][contactID]
}
onChange={() => toggleCheckedUser(company.companyID, contactID)}
/>
UPDATE, A POSSIBLE SOLUTION
I found that the following works:
To be able to access the current value from useState I used useRef
const selectedSendToRef = useRef();
useEffect( () => {
selectedSendToRef.current = selectedSendTo;
}, [selectedSendTo])
Then inside of my function, I can use selectedSendToRef.current to access the most recent value of `selectedSendTo.
When updating state, I can access the most recent version from state using
setSelectedSendTo( prevValue => ....)
const toggleCheckedUser = (companyID, contactID) => {
console.log(companyID, contactID, selectedSendToRef.current);
console.log('selectedSendTo[companyID]: ', selectedSendTo[companyID]);
let newValue;
if(selectedSendToRef.current[companyID] &&
selectedSendToRef.current[companyID][contactID] === true){
newValue = false;
}else{
newValue = true;
}
setSelectedSendTo(prevValue => (
{
...prevValue,
[companyID]:{
...prevValue[companyID],
[contactID]: newValue,
}
}
));
}
UPDATE 2: The Real Solution
Okay so it seems like the problem was that even after a render, the child component was not receiving the updated state because of how I had used nested functions to create the elements.
Here is how I had things
<Main Component>
<div>
{Object_1}
<div>
</Main Componenent
and object_1 was defined something like this:
const Object_1 =
<React.Fragment>
<h1>Random Header</h1>
{StateObject_Containg_Elements}
</React.Fragment>
Now to create the state object that conatined the elements I wanted to display I was using a funciton called by a useEffect hook. Basically when the server sent back data that I needed, I would tell the useEffect hook to run a function called createElements
const createElements = (data) => {
const elements = Object.keys(data).map( item => return(
<ul>
{subFunction1(item)}
</ul>
subFunction1(item){
item.contacts.map( name => {
reutrn <CustomCheckbox name={name} checked={selectedSendTo[name]}
})
}
saveElementsToState(elements);
}
As you can see we basically have a function that runs 1 time (on server response) that triggers a function that creates the array of elements that we want to display which has its own nested subfunction that includes the child component that we are asking to watch a different state object to know whether it should be checked or not.
So What I did was simplify things, I turned {Object_1} into it's own functional component, lets call it <Object1 />. Inside the component instead of calling a function I just put the function code in there to loop through and return the elements (no longer saving elements to state) and lastly I no longer needed the useEffect since just updating the state object with the data once it gets it from the server would cause my subcomponent to re-render and create the elements. Inside the sub-component I simply return null if the data in state is null.
That fixed all my problems.
so now it looks something like this:
const Object1 = () => {
if(!data)return null;
return(
Object.keys(data).map( item => return(
<ul>
{subFunction1(item)}
</ul>
subFunction1(item){
item.contacts.map( name => {
reutrn <CustomCheckbox name={name} checked={selectedSendTo[name]}
})
}
)
}
return(
<div>
<Object1 /> //This is what contains/creates the elements now
</div>
)