I made a component that has a checkbox that when checked should trigger the addition of the same exact component to an array.
As shown below:
The problem is when I try to add another component by checking the "Another color?" checkbox from the newly added component it doesn’t work.
It only adds one component item to the array but doesn’t add more than that.
The ColorQuantity component
const ColorQuantity = ({anotherColorCallbk,reverseColorCallbk,anotherSizeCallbk})=>{
const [prodColor,setProdColor] = useState('');
const [isAnotherColor,setIsAnotherColor] = useState(false);
const [prodSize,setProdSize] = useState('none');
const [isAnotherSize,setIsAnotherSize] = useState(false);
const [prodQuantity,setProdQuantity] = useState(0);
const anotherColorHandler = e =>{
setIsAnotherColor(e.target.checked);
if(!isAnotherColor){
anotherColorCallbk();
}
else{
reverseColorCallbk();
}
}
const anotherSizeHandler = e =>{
if(prodColor.length > 0){
setIsAnotherSize(e.target.checked);
anotherSizeCallbk();
}
}
return (
<div className="color-quantity flex gap-4">
<div className="color-input flex flex-col">
<label className="inpt-label">Product Color</label>
<input type="text" value={prodColor} onChange={e=>setProdColor(e.target.value)} placeholder="Enter the product color." className="inpt" />
<div className='moreColor'>
<input type="checkbox" checked={isAnotherColor} onChange={anotherColorHandler} className='me-3' />
<label>Another color?</label>
</div>
</div>
<div className="size-input flex flex-col">
<label className="inpt-label">Product Size</label>
<div className="select-container relative text-white">
<select name="sizes" value={prodSize} onChange={e=>setProdSize(e.target.value)} id="prodSizes" className='appearance-none outline-none bg-slate-700 py-2 rounded-lg ps-3 pe-10 font-bold focus:border-0 cursor-pointer'>
<option value="none">Select Size</option>
<option value="xs">XS</option>
<option value="s">S</option>
<option value="m">M</option>
<option value="l">L</option>
<option value="xl">XL</option>
<option value="xxl">XXL</option>
</select>
<div className="icon-container">
<FontAwesomeIcon className='absolute right-3 top-3' icon={faAngleDown}></FontAwesomeIcon>
</div>
</div>
<div className='moreSize'>
<input type="checkbox" checked={isAnotherSize} onChange={anotherSizeHandler} className='me-3' />
<label>Another size?</label>
</div>
</div>
<div className="quantity-input flex flex-col">
<label className="inpt-label">Product Quantity</label>
<input type="number" value={prodQuantity} onChange={e=>setProdQuantity(e.target.value)} placeholder="Enter size quantity." className="inpt" />
</div>
</div>
);
}
The parent component
export const AddProduct = ()=>{
const [colorQList, setColorQList] = useState([]);
const anotherColorCallbkHandler = () =>{
setColorQList([...colorQList,
<ColorQuantity key={colorQList.length}
anotherColorCallbk={anotherColorCallbkHandler} //this one doesn't work
reverseColorCallbk={reverseColorCallbkHandler}
anotherSizeCallbk={anotherSizeCallbkHandler}/>]);
console.log('checked!!')
}
const reverseColorCallbkHandler = ()=>{
const colorInputList = [...colorQList];
colorInputList.pop();
setColorQList(colorInputList);
}
const anotherSizeCallbkHandler = () =>{
}
return (
<div>
<ColorQuantity anotherColorCallbk={anotherColorCallbkHandler} reverseColorCallbk={reverseColorCallbkHandler} anotherSizeCallbk={anotherSizeCallbkHandler}/>
{colorQList}
</div>
);
}
I tried to use the recursive function so that it calls the function which add components into array inside the function itself but it didn’t seem to work.
>Solution :
Your problem is that you’re storing JSX in your state. There are not many scenarios where you need to do that. At the moment, when you add <ColorQuantity ... /> to your state in the anotherColorCallbkHandler function, and pass the prop anotherColorCallbk={anotherColorCallbkHandler} to it, the anotherColorCallbkHandler function that you’re passing is the first function reference that was created on the first render, which only knows about the initial state value. That means that future invocations of this function will result in the initial state being updated even though the state has since changed. The easiest fix would be to change your function to use the state updater function to avoid the "stale state" issue by getting access to the latest state value:
setColorQList(colorQList => [...colorQList,
<ColorQuantity key={colorQList.length}
anotherColorCallbk={anotherColorCallbkHandler}
reverseColorCallbk={reverseColorCallbkHandler}
anotherSizeCallbk={anotherSizeCallbkHandler}
/>
]);
With this change, the anotherColorCallbkHandler doesn’t rely on state outside of its scope, and instead gets access to the current state from the state updater function, avoiding the stale state issue. However, I would advise against this solution and instead avoid storing JSX in your state to begin with.
To avoid storing JSX in your state, you update it to hold data that tells your render (returned) JSX how to display your UI. Your JSX can map over your state to render the appropriate components. For example:
const AddProduct = ()=>{
const [colorQList, setColorQList] = useState(() => [crypto.randomUUID()]); // start off the state with a random id in the array so that the returned JSX can display it
const anotherColorCallbkHandler = () => {
const newColorQId = crypto.randomUUID(); // get a random ID for the new question (this is used as the `key` in the returned JSX)
setColorQList(colorQList => [...colorQList, newColorQId]); // I still suggest using the state setter function here, although it's not required
console.log('checked!!');
}
const reverseColorCallbkHandler = () => {
// Consider using `.filter()` here to update to remove a particular question by its id. At the moment this just removes the last one, even if it's not the last question checkbox that was unchecked.
if (colorQList.length > 1) { // only allow removal of the question if there is more than 1 item.
const colorInputList = [...colorQList];
colorInputList.pop();
setColorQList(colorInputList);
}
}
const anotherSizeCallbkHandler = () => {
// ... your implementation ...
}
return (
<div>
{colorQList.map(id => <ColorQuantity
key={id}
anotherColorCallbk={anotherColorCallbkHandler}
reverseColorCallbk={reverseColorCallbkHandler}
anotherSizeCallbk={anotherSizeCallbkHandler}
/>)}
</div>
);
}
