NoneUse LoopCallback - хук useCallback для компонентов, созданных внутри цикла

Я хотел бы начать обсуждение рекомендуемого подхода для создания обратных вызовов, которые принимают параметр из компонента, созданного внутри цикла.

Например, если я заполняю список элементов, у которых будет кнопка "Удалить", я хочу, чтобы обратный вызов "onDeleteItem" знал индекс удаляемого элемента. Так что-то вроде этого:

  const onDeleteItem = useCallback(index => () => {
    setList(list.slice(0, index).concat(list.slice(index + 1)));
  }, [list]);

  return (
    <div>
      {list.map((item, index) =>
        <div>
          <span>{item}</span>
          <button type="button" onClick={onDeleteItem(index)}>Delete</button>
        </div>
      )}
    </div>
  ); 

Но проблема в том, что onDeleteItem всегда будет возвращать новую функцию в обработчик onClick, вызывая повторную визуализацию кнопки, даже если список не изменился. Так что это побеждает цель useCallback,

Я придумал свой собственный хук, который я назвал useLoopCallback, который решает проблему:

import React, {useCallback, useRef} from "react";

export function useLoopCallback(code, dependencies) {
  const callback = useCallback(code, dependencies);
  const ref = useRef({map: new Map(), callback});
  const store = ref.current;

  if (callback !== store.callback) {
    store.map.clear();
    store.callback = callback;
  }

  return loopParam => {
    let loopCallback = store.map.get(loopParam);
    if (!loopCallback) {
      loopCallback = () => callback(loopParam);
      store.map.set(loopParam, loopCallback);
    }
    return loopCallback;
  }
}

Теперь вышеприведенный обработчик выглядит так:

  const onDeleteItem = useLoopCallback(index => {
    setList(list.slice(0, index).concat(list.slice(index + 1)));
  }, [list]);

Это прекрасно работает, но теперь мне интересно, действительно ли эта дополнительная логика ускоряет процесс или просто добавляет ненужные накладные расходы. Может ли кто-нибудь, пожалуйста, дать некоторое представление?

РЕДАКТИРОВАТЬ: альтернатива вышеупомянутому - это обернуть элементы списка внутри их собственного компонента. Так что-то вроде этого:

function ListItem({key, item, onDeleteItem}) {
  const onDelete = useCallback(() => {
    onDeleteItem(key);
  }, [onDeleteItem, key]);

  return (
    <div>
      <span>{item}</span>
      <button type="button" onClick={onDelete}>Delete</button>
    </div>
  );
}

export default function List(...) {
  ...

  const onDeleteItem = useCallback(index => {
    setList(list.slice(0, index).concat(list.slice(index + 1)));
  }, [list]);

  return (
    <div>
      {list.map((item, index) =>
        <ListItem key={index} item={item} onDeleteItem={onDeleteItem} />
      )}
    </div>
  ); 
}

2 ответа

Оптимизация производительности всегда идет с затратами. Иногда эта стоимость ниже, чем оптимизируемая операция, иногда выше. useCallback это крюк очень похож на useMemoна самом деле вы можете думать об этом как о специализации useMemo это можно использовать только в функциях. Например, нижеприведенные выражения являются эквивалентами

const callback = value => value * 2

const memoizedCb = useCallback(callback, [])
const memoizedwithUseMemo = useMemo(() => callback(), [])

Так что пока на каждое утверждение о useCallback может применяться к useMemo,

Суть memoization сохранить копии старых значений для возврата в случае, если мы получим те же зависимости, это может быть здорово, если у вас есть что-то, что expensive вычислить. Посмотрите на следующий код

const Component = ({ items }) =>{
    const array = items.map(x => x*2)
}

Опровергать каждый render const array будет создан в результате map выполняется в items, Таким образом, вы можете испытать искушение сделать следующее

const Component = ({ items }) =>{
    const array = useMemo(() => items.map(x => x*2), [items])
}

В настоящее время items.map(x => x*2) будет выполняться только тогда, когда items изменить, но стоит ли? Короткий ответ: нет. Производительность, полученная при этом, тривиальна и иногда будет более дорогой в использовании memoization чем просто выполнить функцию каждого рендера. Оба крючка (useCallback а также useMemo) полезны в двух разных случаях использования:

  • Ссылочное равенство

Когда вам нужно убедиться, что ссылочный тип не вызовет рендеринг только из-за сбоя shallow comparison

  • Вычислительно дорогие операции(только useMemo)

Что-то вроде этого

const serializedValue = {item: props.item.map(x => ({...x, override: x ? y : z}))}

Теперь у вас есть причина memoized операция и лениво восстановить serializedValue каждый раз props.item изменения:

const serializedValue = useMemo(() => ({item: props.item.map(x => ({...x, override: x ? y : z}))}), [props.item])

Любой другой вариант использования почти всегда стоит просто заново вычислить все значения, React это довольно эффективно, и дополнительные рендеры почти никогда не вызывают проблем с производительностью. Имейте в виду, что иногда ваши усилия по оптимизации вашего кода могут пойти другим путем и создать много лишнего / ненужного кода, который не будет приносить столько пользы (иногда только вызовет больше проблем).

Компонент List управляет своим собственным состоянием (списком), функции удаления которого зависят от того, доступен ли этот список при его закрытии. Поэтому, когда список меняется, функция удаления должна измениться.

С редуксом это не будет проблемой, потому что удаление элементов будет выполнено путем отправки действия и будет изменено редуктором, который всегда выполняет одну и ту же функцию.

У React есть хук useReducer, который вы можете использовать:

import React, { useMemo, useReducer, memo } from 'react';

const Item = props => {
  //calling remove will dispatch {type:'REMOVE', payload:{id}}
  //no arguments are needed
  const { remove } = props;
  console.log('component render', props);
  return (
    <div>
      <div>{JSON.stringify(props)}</div>
      <div>
        <button onClick={remove}>REMOVE</button>
      </div>
    </div>
  );
};
//wrap in React.memo so when props don't change
//  the ItemContainer will not re render (pure component)
const ItemContainer = memo(props => {
  console.log('in the item container');
  //dispatch passed by parent use it to dispatch an action
  const { dispatch, id } = props;
  const remove = () =>
    dispatch({
      type: 'REMOVE',
      payload: { id },
    });
  return <Item {...props} remove={remove} />;
});
const initialState = [{ id: 1 }, { id: 2 }, { id: 3 }];
//Reducer is static it doesn't need list to be in it's
// scope through closure
const reducer = (state, action) => {
  if (action.type === 'REMOVE') {
    //remove the id from the list
    return state.filter(
      item => item.id !== action.payload.id
    );
  }
  return state;
};
export default () => {
  //initialize state and reducer
  const [list, dispatch] = useReducer(
    reducer,
    initialState
  );
  console.log('parent render', list);
  return (
    <div>
      {list.map(({ id }) => (
        <ItemContainer
          key={id}
          id={id}
          dispatch={dispatch}
        />
      ))}
    </div>
  );
};
Другие вопросы по тегам