Dynamically generated checkbox list performance issues (React)

Viewed 93

I am working on a reusable filter component that dynamically creates a list of checkboxes within a dropdown, and includes an "all selected" toggle (similar to google flights). Everything is working as desired, but there is huge latency when clicking a checkbox, and especially when using the select all toggle. It is very similar to the issue described here (almost identical even), but I have tried my best to implement the solutions described therein to no avail. I tried applying useCallback on all the functions that are passed, and gotten rid of all warnings from 'why-did-you-render' but none of it resolved the issue. I'm hoping somebody here can help tell me what I'm missing. See relevant code snippets below. Thanks!

This is the parent component that manages the data:

import React, { useState, useEffect } from "react";
import ControlPanel from '../Components/ControlPanel';

const TopLevelComponent = () => {

    const [data, setData] = useState();
    const [selectedFilters, setSelectedFilters] = useState([]);

    useEffect(() => {
        getData();
    }, [])

    const getData = async() => {
        ...
    }

    return (
        <div>
            <ControlPanel
                filterOptions={ data?.filterSet }
                filterSelections={ selectedFilters }
                filterHandler={ setSelectedFilters }
            />

            { /* other stuff irrelevant here */ }
          
        </div>
    )
}

export default TopLevelComponent;

This is a reusable control panel component that holds the filters and other settings:

import React from "react";
import { Filter } from './Inputs';

const ControlPanel = ({ filterOptions, filterSelections, filterHandler }) => {

    return (
        <>

            { /* other controls not relevant here */ }

            <div className="control-panel__filters">
                <Filter 
                  filterName="Filter A" 
                  options={ filterOptions } 
                  handler={ filterHandler } 
                  selections={ filterSelections } 
                  selectAllLabel="Select all filter A" 
                />     
            </div>

            { /* more irrelevant stuff */ }

        </>
    )
}

export default ControlPanel;

and finally, this is where the filters and the components that make them up are defined:

import React, { useState } from "react";
import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem, Label } from 'reactstrap';
import Toggle from 'react-toggle';
import chevronDown from '../icons/chevron-down-dark.svg';
import chevronUp from '../icons/chevron-top.svg';
import closeIcon from '../icons/close.svg';

export const Filter = ({ options, handler, selections, filterName, selectAllLabel }) => {
    
    const [dropdownOpen, setDropdownOpen] = useState(false);

    const toggle = () => setDropdownOpen(prevState => !prevState);
  
    return (
        <Dropdown isOpen={ dropdownOpen } toggle={ toggle } >
            <DropdownToggle>
                { filterName }
                <img src={ dropdownOpen ? chevronUp : chevronDown } />
            </DropdownToggle>
            <DropdownMenu>
                <FilterInterface 
                    toggle={ toggle } 
                    filterName={ filterName } 
                    options={ options } 
                    selections={ selections } 
                    handler={ handler } 
                    selectAllLabel={ selectAllLabel }
                />
            </DropdownMenu>
        </Dropdown>
    );
}

const FilterInterface = ({ toggle, filterName, options, selections, handler, selectAllLabel }) => {

    const [allSelected, setAllSelected] = useState(false);

    const toggleAllSelected = () => {
        if (options.length !== selections.length) {
            const newSelections = options.map(option => option.id);
            handler(newSelections);
            setAllSelected(true);    
        } else {
            handler([]);
            setAllSelected(false);
        }
    }

    const handleCheck = id => {
        let newArray;
        if (selections.includes(id)) {
            newArray = selections.filter(selection => {
                return selection !== id;
            });
            setAllSelected(false);
        } else {
            newArray = [...selections, id];
        }
        handler(newArray);
        if (newArray.length === options.length) setAllSelected(true);
    }

    return (
        <>
            <DropdownItem header>
                { filterName }
                <img src={ closeIcon } onClick={ toggle } />
            </DropdownItem>
            <DropdownItem header>
                {selectAllLabel}
                <Toggle
                    checked={ allSelected }
                    icons={ false }
                    onChange={ toggleAllSelected }
                />
            </DropdownItem>
            {
                options?.map(option => (
                    <DropdownItem header
                        key={ option.id }
                        value={ option.name }
                        className="filterCheckbox"
                    >
                        <Label onClick={ () => handleCheck(option.id) }>
                            <Checkbox checked={ selections.includes(option.id) } readOnly />
                            <span>{ option.name }</span>
                        </Label>
                    </DropdownItem>
                ))
            }
        </>
    )
}
1 Answers

The thing that finally seemed to do the trick that isn't discussed in the linked article was passing custom comparison functions to the memoized components so they would not update when the dropdown was closed.

Related