I have created a functional component which belongs to the sidebard. It contains of 3 components.
ProductFilters - main list component, fetches possible data filters from the server, provides a filters state which can be updated by child components when a filter is selected.
FilterOptions - filter category
Option - represents each option in filter menu. When is selected, it calls ProductFilters callback to update the filters list. After selecting it, the filters
map is being updated and... the selected Option
element dissapears. After selecting the empty place, react throws and error
index.js:1 Warning: Each child in a list should have a unique "key" prop. Check the render method of
FilterOptions
Each Option
element has it's own unique key which is the option name. What might be the issue?
Here is my code:
import axios from "axios";
import List from "@mui/material/List";
import ListItemButton from "@mui/material/ListItemButton";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
import Collapse from "@mui/material/Collapse";
import InboxIcon from "@mui/icons-material/MoveToInbox";
import ExpandLess from "@mui/icons-material/ExpandLess";
import ExpandMore from "@mui/icons-material/ExpandMore";
import Button from "@mui/material/Button";
const Option = (props) => {
const [selected, setSelected] = useState(false);
const handleClick = () => {
setSelected(!selected);
props.setFilter(props.category, props.name, !selected);
};
return (
<ListItemButton
selected={selected}
sx={{ pl: 8 }}
onClick={handleClick}
>
<ListItemText primary={props.name} />
</ListItemButton>
);
};
const FilterOptions = (props) => {
const [open, setOpen] = useState(false);
const handleClick = () => {
setOpen(!open);
};
const displayOptions = () => {
const options = [];
for (const [key, option] of props.filter) {
options.push(
<Option
key={key}
name={option.name}
category={props.name}
setFilter={props.setFilter}
/>
);
}
return options;
};
return (
<Fragment>
<ListItemButton sx={{ pl: 4, fontSize: 12 }} onClick={handleClick}>
<ListItemText primary={props.name} />
</ListItemButton>
<Collapse in={open} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{displayOptions()}
</List>
</Collapse>
</Fragment>
);
};
export default function ProductFilters(props) {
const [open, setOpen] = useState(false);
const [filters, setFilters] = useState(new Map());
const setFilter = (category, filter, selected) => {
const options = filters.get(category).set(filter, selected);
setFilters(new Map(filters.set(category, options)));
};
useEffect(() => {
const fetchFilters = async () => {
try {
const res = await axios.get(
`${process.env.REACT_APP_API_URL}/api/product/filters`
);
const outputFilters = new Map();
res.data.map((filter) => {
const options = new Map();
filter.taxonomydata_set.map((option) => {
options.set(option.name, {
name: option.name,
selected: false,
});
});
outputFilters.set(filter.category, options);
});
setFilters(outputFilters);
} catch (err) {}
};
fetchFilters();
}, []);
const handleClick = () => {
setOpen(!open);
};
const displayFilters = () => {
let filterList = [];
for (const [categoryName, filterCat] of filters) {
filterList.push(
<FilterOptions
key={categoryName}
name={categoryName}
filter={filterCat}
setFilter={setFilter}
/>
);
}
return filterList;
};
const handleApplyFilters = () => {
console.log(filters);
};
return (
<List
sx={{
width: "100%",
maxWidth: 260,
bgcolor: "background.paper",
}}
component="nav"
aria-labelledby="nested-list-subheader"
>
<ListItemButton onClick={handleClick}>
<ListItemIcon>
<InboxIcon />
</ListItemIcon>
<ListItemText primary={props.ListTitle} />
{open ? <ExpandLess /> : <ExpandMore />}
</ListItemButton>
<Collapse in={open} timeout="auto" unmountOnExit>
<List>{displayFilters()}</List>
</Collapse>
<Button variant="contained" onClick={handleApplyFilters}>
Apply filters
</Button>
</List>
);
}
Link to the code on codesanbox
CodePudding user response:
I think you should apply the 'key' to the Fragment
const Option = (props) => {
...
return (
<ListItemButton
key={props.name}
selected={selected}
sx={{ pl: 8 }}
onClick={handleClick}
>
<ListItemText primary={props.name} />
</ListItemButton>
);
};
const FilterOptions = (props) => {
....
return (
<Fragment key={props.name}>
...
</Fragment>
);
}
CodePudding user response:
Problem solved. The issue wasn't the keys, they were set correctly.
The problem was in handler function. I had overwrite the map element type from JSON to boolean (selected) which caused that my Option
component was not keyed anymore with props.name during re-render because the name was removed and replaced by boolean variable.
From:
const setFilter = (category, filter, selected) => {
const options = filters.get(category).set(filter, selected);
setFilters(new Map(filters.set(category, options)));
};
changed to
const setFilter = (category, filter, selected) => {
const option = filters.get(category).get(filter);
option.selected = selected;
const options = filters.get(category).set(filter, option);
setFilters(new Map(filters.set(category, options)));
};