React: оптимизация (повышение производительности) формы с большим кол-вом Checkbox элементов

В продолжении темы React + Ant design: задержки при отображении и взаимодействии с большим (200+) кол-вом Checkbox

Поясните пожалуйста как в данном примере организовать использование хуков React типа useMemo, useCallback и метода memo для снижения кол-ва отрисовки компонентов.

Задача:

  1. есть компонент (ComponentListModal), который работает с выбранными элементами
  2. компонент (ComponentListByAlphabet), вызываемый из 1), который отрисовывает список сгруппированных checkbox
  3. компонент (ComponentListLetterBlock), вызываемый из 2), который отрисовывает группу checkbox
  4. компонент (ComponentListLetterItem), вызываемый из 3), который отрисовывает отдельный checkbox

Проблема: при изменении состояния отдельного чекбокса в 4) изменяется список выбранных элементов в 1) и происходит перерисовка всех!!! чекбоксов, хотя по идее надо бы перерисовать только один чекбокс.

Код (выкинул для экономии места непринципиальный для текущего вопроса функционал) выглядит примерно так:

// компонент: элемент группы списка
const ComponentListLetterItem = (props: IProps_ListLetterItem) => {
    // отрисовать компонент
    return (
        <div className='lm-block-item'>
            <Checkbox
                checked     = {props.checked}
                onChange    = {(e: CheckboxChangeEvent) => props.onCheck(props.item.id, e?.target?.checked)}
            >
                <div className="lm-item-name">{props.item.name}</div>
            </Checkbox>
        </div>    
    );
}


// компонент: группа списка
const ComponentListLetterBlock = (props: IProps_ListLetterBlock) => {
    // отрисовать компонент
    return (
        <div className='lm-block'>
            <h3 className="lm-block-letter">{props.group.id}</h3>
            {
                props.group.values.map((elem, index) => (
                    <ComponentListLetterItem
                        key         = {index}
                        item        = {elem}
                        checked     = {props.selectedIDs.includes(elem.id)}
                        onCheck     = {props.onCheck}
                    />
                ))
            }
        </div>
    );
}


// компонент: список, сгруппированный по алфавиту
const ComponentListByAlphabet = (props: IProps_ListAlphabet) => {    
    // группировка элементов по алфавиту
    const groups = groupItemsByLetter(props.items);
  
    // отрисовать компонент
    return (
        <div className='lm-panel'>
            <div className='lm-list'>
                {
                    groups.map((group, index) => (
                        <ComponentListLetterBlock 
                            key         = {index} 
                            group       = {group} 
                            selectedIDs = {props.selectedIDs} 
                            onCheck     = {props.onCheckItem}
                        />
                    ))
                }
            </div>
        </div>
    );
}


// копмпонент: окно выбора элементов списка
const ComponentListModal = (props: IProps_ListModal) => {
    // контроль состояний
    const [selectedItems, setSelectedItems] = useState<string[]>([]);      // список
   
    // событие: выбран элемент
    const handleCheckItem = (id: string, checked: boolean) => {
        if (checked) {
            // добавить идентификатор элемента в список выбранных элементов
            setSelectedItems([...selectedItems.filter(elem => elem !== id), id]);
        }
        else {
            // удалить идентификатор элемента из списка выбранных элементов
            setSelectedItems(selectedItems.filter(elem => elem !== id));
        }
    }
       
    // отрисовать компонент
    return (
        <div className="lm-content">
            <ComponentListByAlphabet
                items           = {props.items}
                selectedIDs     = {selectedItems}
                onCheckItem     = {handleCheckItem}
            />
        </div>
    );
}

Подскажите как лучше всего было бы оптимизировать данный React + Ant Design + typescript код.

Вроде как просится memo для ComponentListLetterItem, но чтоб он сработал нужно завернуть в useCallback функцию handleCheckItem и вот тут недопонимание, потому что при таком решении в лоб в списке выбирается исключительно только 1 элемент, при выборе 2 элемента галка с первого снимается


Ответы (1 шт):

Автор решения: ksa

Поясните пожалуйста как в данном примере организовать использование хуков React типа useMemo, useCallback и метода memo для снижения кол-ва отрисовки компонентов.

Снижение количества рендеров, на моем примере, можно реализовать вот таким образом. Я специально поставил console.log в компонентах для демонстрации.

Сначала выведутся все группы и все чеки... Но потом будут ренделиться только компонент "выбраной" группы и компонент измененного чека.

Поскольку твой "пример" не рабочий и просто "набор" кусков... На нем показать что-то работающее не представляется возможным.

//
const Itm = React.memo(({obj, n, act}) => {
  console.log(obj.id)
  const fn = () => act(n, obj.id)
  return <li>
    <input type='checkbox' onChange={fn} checked={obj.v} />
  </li>
})
//
const Grp = React.memo(({obj, act}) => {
  console.log(obj.name)
  return <li>
    <p>{obj.name}</p>
    <ol>
      {obj?.arr.map(o => <Itm key={o.id} obj={o} n={obj.id} act={act} />)}
    </ol>
  </li>
})
//
const App = () => {
  const {arr, act} = useData()
  return <ul>
    {arr.map(o => <Grp key={o.id} obj={o} act={act} />)}
  </ul>
}

const domContainer = document.querySelector('#like_button_container');
const root = ReactDOM.createRoot(domContainer);
root.render(<App />);
//
function useData(){
  const [arr, setArr] = React.useState([])
  React.useEffect(_ => setArr(data(10, 30)), [])
  const act = React.useCallback((n, m) => setArr(old => old.map(o => o.id === n
    ? {...o, arr: o.arr.map(o => o.id === m
      ? {...o, v: !o.v}
      : o
    )}
    : o
  )), [])
  return {arr, act}
}
//
function data(n, m){
  return Array.from({length: n}, (_, i) => {
    return {
      id: i,
      name: 'Группа ' + (i + 1),
      arr: Array.from({length: m}, (_, i) => {
        return {
          id: i,
          v: false
        }
      })
    }
  })
}
ul,
ol { 
  list-style-type: none;
  padding-left: 0;
}
ol {
  display: flex;
}
<div id="like_button_container"></div>
<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>

→ Ссылка