Reputation: 366
I have a parent component that I want to use to dynamically add and remove an stateful component, but something weird is happening.
This is my parent component and how I control my list of FilterBlock
import React from 'react'
import FilterBlock from './components/FilterBlock'
export default function List() {
const [filterBlockList, setFilterList] = useState([FilterBlock])
const addFilterBlock = () => {
setFilterList([...filterBlockList, FilterBlock])
}
const onDeleteFilterBlock = (index) => {
const filteredList = filterBlockList.filter((block, i) => i !== index);
setFilterList(filteredList)
}
return (
<div>
{
filterBlockList.map((FilterBlock, index) => (
<FilterBlock
key={index}
onDeleteFilter={() => onDeleteFilterBlock(index)}
/>
))
}
<button onClick={addFilterBlock}></button>
</div>
)
}
You can assume FilterBlock
is a stateful react hooks component.
My issue is whenever I trigger the onDeleteFilter
from inside any of the added components, only the last pushed FilterBlock
gets removed from the list even though I remove it based on its index on the list.
Please check my example here:
https://jsfiddle.net/emad2710/wzh5Lqj9/10/
Upvotes: 3
Views: 311
Reputation: 8489
The problem is you're storing component function FilterBlock
to the state and mutate the parent state inside a child component.
You may know that mutating state directly will cause unexpected bugs. And you're mutating states here:
/* Child component */
function FilterBlock(props) {
const [value, setValue] = React.useState('')
const options = [
{ value: 'chocolate', label: 'Chocolate' },
{ value: 'strawberry', label: 'Strawberry' },
{ value: 'vanilla', label: 'Vanilla' }
]
const onInputChange = (e) => {
// child state has been changed, but parent state
// does not know about it
// thus, parent state has been mutated
setValue(e.target.value)
}
return (
<div>
<button onClick={props.onDeleteFilter}>remove</button>
<input value={value} onChange={onInputChange}></input>
</div>
)
}
/* Parent component */
const addFilterBlock = () => {
setFilterList([...filterBlockList, FilterBlock])
}
filterBlockList.map((FilterBlock, index) => (
<FilterBlock
key={index}
onDeleteFilter={() => onDeleteFilterBlock(index)}
/>
))
When a FilterBlock value changes, FilterBlock
will store its state somewhere List
component cannot control. That means filterBlockList
state has been mutated. This leads to unpredictable bugs like the above problem.
To overcome this problem, you need to manage the whole state in parent component:
const {useCallback, useState} = React;
function FilterBlock(props) {
const options = [
{ value: 'chocolate', label: 'Chocolate' },
{ value: 'strawberry', label: 'Strawberry' },
{ value: 'vanilla', label: 'Vanilla' }
]
return (
<div>
<button onClick={props.onDeleteFilter}>remove</button>
<input value={props.value} onChange={props.onChange}></input>
</div>
)
}
function App() {
const [values, setValues] = useState(['strawberry']);
const addFilterBlock = useCallback(() => {
setValues(values.concat(''));
}, [values]);
const onDeleteFilterBlock = useCallback((index) => {
setValues(values.filter((v, i) => i !== index));
}, [values]);
const setFilterValue = useCallback((index, value) => {
const newValues = [...values];
newValues[index] = value;
setValues(newValues);
}, [values]);
return (
<div>
{
values.map((value, index) => (
<FilterBlock
key={index}
onDeleteFilter={() => onDeleteFilterBlock(index)}
value={value}
onChange={(e) => setFilterValue(index, e.target.value)}
/>
))
}
<button onClick={addFilterBlock}>add</button>
</div>
)
}
ReactDOM.render(<App />, document.querySelector("#app"))
Upvotes: 1