I have a shopping catalog where the checkboxes filter which products are rendered.
The filters
list is the menu and also passed in as a state.
The products
list is the actual products which are rendered after filtered by looking at which category is checked or selected as "true".
const filters = [
{
"name": "Food",
"selected": true
},
{
"name": "Books",
"selected": true
},
{
"name": "Cars",
"selected": true
}
]
const products = [
{
"name": "Sandwich",
"catagory": "Food",
"price": 2
},
{
"name": "Soup",
"catagory": "Food",
"price": 5
},
{
"name": "Toyota",
"catagory": "Cars",
"price": 25
},
{
"name": "Tesla",
"catagory": "Cars",
"price": 100
},
{
"name": "BMW",
"catagory": "Cars",
"price": 69420
}
]
export default function TestCatalog( props ) {
const [menuFilters, setMenuFilters] = useState(filters)
function handleChangeMenu(event) {
let newSelected = menuFilters
newSelected.forEach(filterChange => {
if (filterChange.name == event.target.name) {
filterChange.selected = event.target.checked
}
})
setMenuFilters(newSelected)
}
function buildMenuItems(items) {
return (
items.map(value =>
<FormControlLabel control={<Checkbox defaultChecked />}
name={value.name}
onChange={event => handleChangeMenu(event)}
label={value.name} />
)
)
}
function buildCatalogGrid() {
let listItemsSelected = []
menuFilters.map(obj => {
if (obj.selected) {
listItemsSelected.push(obj.["name"])
}
})
let productsToRender = products.filter(
product => listItemsSelected.includes(product.catagory)
)
return (
productsToRender.map(value =>
<Grid item xs={12} sm={6} md={4}>
<CatalogCard title={value.name}
body={value.price}>
</CatalogCard>
</Grid>
)
)
}
return (
<Grid container>
<Grid item>
<FormGroup>
{buildMenuItems(filters)}
</FormGroup>
</Grid>
<Grid item>
<Grid container>
{buildCatalogGrid()}
</Grid>
</Grid>
</Grid>
)
}
In the handler for the checkbox, I copy the state, make my change, then update the state with the copy.
There are two issues:
- The products rendered does not change.
- It appears editing the copy changes the state itself.
...
function handleChangeMenu(event) {
let newSelected = menuFilters
newSelected.forEach(filterChange => {
if (filterChange.name == event.target.name) {
filterChange.selected = event.target.checked
}
})
setMenuFilters(newSelected)
}
}
...
All the solutions I've read so far say I have to use the setState which I do but the products still don't render.
I'm using Next JS.
CodePudding user response:
You are mutating the filters
state, and then saving the same reference back into state, so React is bailing on rerenders.
function handleChangeMenu(event) {
let newSelected = menuFilters // <-- reference to state
newSelected.forEach(filterChange => {
if (filterChange.name == event.target.name) {
filterChange.selected = event.target.checked // <-- state mutation!!
}
})
setMenuFilters(newSelected) // <-- reference saved back into state
}
You should shallow copy the previous state into a new object state reference for React to "see" that state has updated and trigger a rerender. Use a functional state update to shallow copy (map) the previous state to the next, and when updating the specific filter object, shallow copy it into a new object reference.
function handleChangeMenu(event) {
const { checked, name } = event.target;
setMenuFilters(filters => filters.map(
filter => filter.name === name
? {
...filter,
selected: checked,
}
: filter)
)
}
CodePudding user response:
You are mutating state, instead treat it as immutable:
function handleChangeMenu(event) {
const {name, checked } = event.target
setMenuFilters(prevFilters =>
prevFilters.map((filter) => {
return filter.name === name
? { ...filter, selected: checked }
: filter;
})
);
}