Dynamic className in map not changing after update - javascript

I'm trying to update my react className when the active changes in the sites variable which is mapped to loop through the items.
What happens is that the className 'inactive' does not go away if the active status changes to true or visa versa.
Code:
// Context: this code is inside of the component
const [sites, setSites] = useState([]); <--- Updated dynamically with fetch()
const changeActive = (id) => {
const tmpSites = sites;
for (const s in tmpSites) {
if (tmpSites[s].id === id) {
tmpSites[s].active = !Boolean(tmpSites[s].active);
}
}
setSites(tmpSites);
};
return (
{sites.length ? sites.map((item, i) => {
return (
<tr className={`${!Boolean(item.active) ? 'inactive' : ''}`} key={item.id}>
// inbetween data
</tr>
)
}) : null}
)

You need to create a copy of the sites array and make changes to the copy and then set it in state. Never mutate state directly as it might not cause a re-render as we are updating the state with the same object reference.
const changeActive = (id) => {
const tmpSites = [...sites];
for (const s in tmpSites) {
if (tmpSites[s].id === id) {
tmpSites[s].active = !Boolean(tmpSites[s].active);
}
}
setSites(tmpSites);
};

Because you are mutating the original sites Object and not cloning it before making the changes, the useState ("setSites") does not actually re-renders the component because it cannot compare previous Object to current, because they are the same.
You must do a deep-clone of the sites Array of Objects:
const changeActive = (id) => {
setSites(sites => {
sites.map(site => ({ // ← first-level clone
...site // ← second-level clone
active: site.id === id ? !site.active : site.active
}))
})
}
It is imperative to use the setSites function that returns the current state and then you can reliably deep-clone it.

Related

force remount on value change

I have some files that builds a cart in a dropdown for my shop website.
One file adds the selected item to an array which will be my cart. The other file is the CartDropdown component itself. My cart only show the items when I close and open it (remounting), but I want it to remount every time I add a new item.
Adding item function:
const ProductContainer = ({ productInfo }) => {
const { cartProducts, setCartProducts } = useContext(CartContext);
const cartArray = cartProducts;
const addProduct = () => {
productInfo.quantity = 1;
if (cartArray.includes(productInfo)) {
const index = cartArray.findIndex((object) => {
return object === productInfo;
});
cartProducts[index].quantity++;
setCartProducts(cartArray);
} else {
cartArray.push(productInfo);
setCartProducts(cartArray);
}
// setCartProducts(cartArray)
console.log(cartProducts);
// console.log(cartArray)
};
};
dropdown component
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">
{cartProducts.map((product) => (
<div key={product.id}>
<img src={product.imageUrl}></img>
</div>
))}
</div>
<button onClick={cleanCart}>CLEAN CART</button>
<Button children={"FINALIZE PURCHASE"} />
</div>
);
};
How can I force the dropdown to remount every time cartProducts changes?
CART CONTEXT:
export const CartContext = createContext({
isCartOpen: false,
setIsCartOpen: () => { },
cartProducts: [],
setCartProducts: () => { }
})
export const CartProvider = ({ children }) => {
const [isCartOpen, setIsCartOpen] = useState(false)
const [cartProducts, setCartProducts] = useState([])
const value = { isCartOpen, setIsCartOpen, cartProducts, setCartProducts };
return (
<CartContext.Provider value={value}>{children}</CartContext.Provider>
)
}
product context
export const ProductsContext = createContext({
currentProducts: null,
setProducts: () => {}
})
export const ProductsProvider = ({children}) => {
const [currentProducts, setProducts] = useState(shop_data)
const value = {currentProducts, setProducts}
return(
<ProductsContext.Provider value={value}>{children}</ProductsContext.Provider>
)
}
You can change the key prop of the component every time you want to remount. Every time cartProduct changes, update the value of key. You can do that using a useEffect with cartProduct as a dependency.
<CartDropdown key={1} />
to
<CartDropdown key={2} />
Edit for more clarification:
const [keyCount, setKeyCount] = useState(0);
useEffect(() => {
setKeyCount(keyCount+1);
}, [cartProducts]);
<CartDropdown {...otherProps} key={keyCount} />
The first issue I see is that you are not using the callback to set the state inside the context but you are doing cartProducts[index].quantity++ and react docs specify
Do Not Modify State Directly
Also after cartProducts[index].quantity++, you call setCartProducts(cartArray); not with cartProducts which you actually updated (this is also the reason why "if I do usestate(console.log('A'), [cartProducts]) its not triggering everytime i add my cart product". But anyway there is an issue even if you would use cartArray for both:
You shouldn't directly do const cartArray = cartProducts since by doing so cartArray will be a reference to cartProducts (not a copy of it) which also shouldn't be modified (because it would mean that you are modifying state directly).
So first 2 things I recommend you to improve would be:
Initialize cartArray as a cartProducts deep copy (if your cartProducts is an array of objects, spread syntax won't do it). So I would reccomand you to check this question answers for creating a deep copy.
After you make sure that cartArray is a deep copy of cartProducts, doublecheck you use cartArray to create a local newValue then set the state of the context with the same value (so basically:
cartArray[index].quantity++;
setCartProducts(cartArray);
)
The deep copy part also apply for const cleanProducts = currentProducts; (you should also create a deep copy here for cleanProducts, instead of saving the object ref).
If you are not using deep copies, your code might still work in some cases, but you might encounter weird behaviors in some other instances (and thoose are really hard to debug). Therefore is a bad practice in general not using deep copies.

React | Adding and deleting object in React Hooks (useState)

How to push element inside useState array AND deleting said object in a dynamic matter using React hooks (useState)?
I'm most likely not googling this issue correctly, but after a lot of research I haven't figured out the issue here, so bare with me on this one.
The situation:
I have a wrapper JSX component which holds my React hook (useState). In this WrapperComponent I have the array state which holds the objects I loop over and generate the child components in the JSX code. I pass down my onChangeUpHandler which gets called every time I want to delete a child component from the array.
Wrapper component:
export const WrapperComponent = ({ component }) => {
// ID for component
const { odmParameter } = component;
const [wrappedComponentsArray, setWrappedComponentsArray] = useState([]);
const deleteChildComponent = (uuid) => {
// Logs to array "before" itsself
console.log(wrappedComponentsArray);
/*
Output: [{"uuid":"acc0d4c-165c-7d70-f8e-d745dd361b5"},
{"uuid":"0ed3cc3-7cd-c647-25db-36ed78b5cbd8"]
*/
setWrappedComponentsArray(prevState => prevState.filter(item => item !== uuid));
// After
console.log(wrappedComponentsArray);
/*
Output: [{"uuid":"acc0d4c-165c-7d70-f8e-d745dd361b5",{"uuid":"0ed3cc3-
7cd-c647-25db-36ed78b5cbd8"]
*/
};
const onChangeUpHandler = (event) => {
const { value } = event;
const { uuid } = event;
switch (value) {
case 'delete':
// This method gets hit
deleteChildComponent(uuid);
break;
default:
break;
}
};
const addOnClick = () => {
const objToAdd = {
// Generate uuid for each component
uuid: uuid(),
onChangeOut: onChangeUpHandler,
};
setWrappedComponentsArray(wrappedComponentsArray => [...wrappedComponentsArray, objToAdd]);
// Have also tried this solution with no success
// setWrappedComponentsArray(wrappedComponentsArray.concat(objToAdd));
};
return (
<>
<div className='page-content'>
{/*Loop over useState array*/}
{
wrappedComponentsArray.length > 0 &&
<div>
{wrappedComponentsArray.map((props) => {
return <div className={'page-item'}>
<ChildComponent {...props} />
</div>;
})
}
</div>
}
{/*Add component btn*/}
{wrappedComponentsArray.length > 0 &&
<div className='page-button-container'>
<ButtonContainer
variant={'secondary'}
label={'Add new component'}
onClick={() => addOnClick()}
/>
</div>
}
</div>
</>
);
};
Child component:
export const ChildComponent = ({ uuid, onChangeOut }) => {
return (
<>
<div className={'row-box-item-wrapper'}>
<div className='row-box-item-input-container row-box-item-header'>
<Button
props={
type: 'delete',
info: 'Deletes the child component',
value: 'Delete',
uuid: uuid,
callback: onChangeOut
}
/>
</div>
<div>
{/* Displays generated uuid in the UI */}
{uuid}
</div>
</div>
</>
)
}
As you can see in my UI my adding logic works as expected (code not showing that the first element in the UI are not showing the delete button):
Here is my problem though:
Say I hit the add button on my WrapperComponent three times and adds three objects in my wrappedComponentsArray gets rendered in the UI via my mapping in the JSX in the WrapperComponent.
Then I hit the delete button on the third component and hit the deleteChildComponent() funtion in my parent component, where I console.log my wrappedComponentsArray from my useState.
The problem then occurs because I get this log:
(2) [{…}, {…}]
even though I know the array has three elements in it, and does not contain the third (and therefore get an undefined, when I try to filter it out, via the UUID key.
How do I solve this issue? Hope my code and explanation makes sense, and sorry if this question has already been posted, which I suspect it has.
You provided bad filter inside deleteChildComponent, rewrite to this:
setWrappedComponentsArray(prevState => prevState.filter(item => item.uuid !== uuid));
You did item !== uuid, instead of item.uuid !== uuid
Please try this, i hope this works
const deleteChildComponent = (uuid) => {
console.log(wrappedComponentsArray);
setWrappedComponentsArray(wrappedComponentsArray.filter(item => item !== uuid));
};
After update
const deleteChildComponent = (uuid) => {
console.log(wrappedComponentsArray);
setWrappedComponentsArray(wrappedComponentsArray.filter(item => item.uuid !== uuid)); // item replaced to item.uuid
};
Huge shoutout to #Jay Vaghasiya for the help.
Thanks to his expertise we managed to find the solution.
First of, I wasn't passing the uuid reference properly. The correct was, when making the objects, and pushing them to the array, we passed the uuid like this:
const addOnClick = () => {
const objToAdd = {
// Generate uuid for each component
uuid: uuid(),
parentOdmParameter: odmParameter,
onChangeOut: function(el) { onChangeUpHandler(el, this.uuid)}
};
setWrappedComponentsArray([...wrappedComponentsArray, objToAdd]);
};
When calling to delete function the function that worked for us, was the following:
const deleteChildComponent = (uuid) => {
setWrappedComponentsArray(item => item.filter(__item => __item.uuid !== uuid)); // item replaced to item.uuid
};

React child component not re-rendering on updated parent state

I've tried to find a solution to this, but nothing seems to be working. What I'm trying to do is create a TreeView with a checkbox. When you select an item in the checkbox it appends a list, when you uncheck it, remove it from the list. This all works, but the problem I have when I collapse and expand a TreeItem, I lose the checked state. I tried solving this by checking my selected list but whenever the useEffect function runs, the child component doesn't have the correct parent state list.
I have the following parent component. This is for a form similar to this (https://www.youtube.com/watch?v=HuJDKp-9HHc)
export const Parent = () => {
const [data,setData] = useState({
name: "",
dataList : [],
// some other states
})
const handleListChange = (newObj) => {
//newObj : { field1 :"somestring",field2:"someotherString" }
setDataList(data => ({
...data,
dataList: data.actionData.concat(newObj)
}));
return (
{steps.current === 0 && <FirstPage //setting props}
....
{step.current == 3 && <TreeForm dataList={data.dataList} updateList={handleListChange}/>
)
}
The Tree component is a Material UI TreeView but customized to include a checkbox
Each Node is dynamically loaded from an API call due to the size of the data that is being passed back and forth. (The roots are loaded, then depending on which node you select, the child nodes are loaded at that time) .
My Tree class is
export default function Tree(props) {
useEffect(() => {
// call backend server to get roots
setRoots(resp)
})
return (
<TreeView >
Object.keys(root).map(key => (
<CustomTreeNode key={root.key} dataList={props.dataList} updateList={props.updateList}
)))}
</TreeView>
)
CustomTreeNode is defined as
export const CustomTreeNode = (props) => {
const [checked,setChecked] = useState(false)
const [childNodes,setChildNodes] = useState([])
async function handleExpand() {
//get children of current node from backend server
childList = []
for( var item in resp) {
childList.push(<CustomTreeNode dataList={props.dataList} updateList={props.updateList} />)
}
setChildNodes(childList)
}
const handleCheckboxClick () => {
if(!checked){
props.updateList(obj)
}
else{
//remove from list
}
setChecked(!checked)
}
// THIS IS THE ISSUE, props.dataList is NOT the updated list. This will work fine
// if I go to the next page/previous page and return here, because then it has the correct dataList.
useEffect(() => {
console.log("Tree Node Updating")
var isInList = props.dataList.find(function (el) {
return el.field === label
}) !== undefined;
if (isInList) {
setChecked(true);
} else {
setChecked(false)
}
}, [props.dataList])
return ( <TreeItem > {label} </TreeItem> )
}
You put props.data in the useEffect dependency array and not props.dataList so it does not update when props.dataList changes.
Edit: Your checked state is a state variable of the CustomTreeNode class. When a Tree is destroyed, that state variable is destroyed. You need to store your checked state in a higher component that is not destroyed, perhaps as a list of checked booleans.

React state is updating but the component is not

There is a component that maps through an array stored in the state. A button, when it is clicked it updates the state, this action is working.
The problem is that the component is not updating too.
Here is the code:
const MyComponent = () => {
...
const [fields, setFields] = useState([{value: 'test', editable: false},
{value: 'test2', editable: false}]);
...
const toggleClass = (id) => {
const aux = fields;
aux[id].editable = true;
setFields(aux);
}
...
return (
<div>
...
{fields.map((field, id) => {
return (
<div>
<input className={field.editable ? 'class1' : 'class2'} />
<button onClick={() => toggleClass(id)}>click</button>
</div>
);
})}
</div>
);
I put logs and the state (fields) is updated after click to editable = true. But the css class is not changing.
Is there any solution to this issue?
You need to make a copy of your existing state array, otherwise you're mutating state which is a bad practice.
const toggleClass = id => {
const aux = [...fields]; //here we spread in order to take a copy
aux[id].editable = true; //mutate the copy
setFields(aux); //set the copy as the new state
};
That's happening because you are mutating the value of fields, which makes it unsure for React to decide whether to update the component or not. Ideally if you should be providing a new object to the setFields.
So, your toggleClass function should look like something below:
const toggleClass = (id) => {
const aux = [...fields]; //This gives a new array as a copy of fields state
aux[id].editable = !aux[id].editable;
setFields(aux);
}
BTW, I also noticed that you're not assigning a key prop to each div of the the map output. Its a good practice to provide key prop, and ideally keep away from using the index as the key.

How to remove element's ref after filtering it from the list

I'm rendering an array of a 'brick' objects as a simple divs on my component.
i am also saving all the refs of the bricks with useRef, when every brick element is saved וnder it's id in a 'brickRefs' object.
under some conditions i am filtering my array so it will render less 'bricks' on the screen.
my problem is: when filtering the array, instead of removing the element i filtered from the 'brickRefs' object - i see that it's still there with a value of null;
what will be the right way to remove it completely from the ref's object ?
//initial ref and state:
const bricksRef = useRef({});
const [bricks, setBricks] = useState([]);
useEffect(() => {
// initializing bricks data
const bricksData = getBricks(NUMBER_OF_BRICKS);
setBricks(bricksData);
}, [])
// filtering the array under some conditions :
const isHittedBrick = (nextTopPos, nextLeftPos) => {
const brickHitted = Object.entries(bricksRef.current).filter(([key, value]) => {
... returning true under some conditions
}).map(([id]) => id)
setBricks(bricks => bricks.filter(b => !brickHitted.includes(b.id)))
}
//render while setting all divs into the refs object:
return (
<section className='bricks'>
{
bricks.map(b => {
return <div key={b.id} className='brick' ref={ref => bricksRef.current[b.id] = ref}>
{b.id}
</div>
})
}
</section>
)
at the filter, right before return delete the false from ref object
const brickHitted = Object.entries(bricksRef.current).filter(([key, value]) => {
if (false under some conditions) delete bricksRef.current[key] // if I understand correctly and b.id === key
... returning true under some conditions
}).map(([id]) => id)
more about delete https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete

Categories

Resources