Home > database >  React list element disappears in grandchild component when grandparent state is updated. Warning: Ea
React list element disappears in grandchild component when grandparent state is updated. Warning: Ea

Time:11-06

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)));
    }; 
  • Related