useEffect - предотвратить бесконечный цикл при обновлении состояния.

9

Я бы хотел, чтобы пользователь мог сортировать список элементов задач. Когда пользователь выбирает элемент из выпадающего списка, он устанавливает, sortKeyкоторый создаст новую версию setSortedTodos, и, в свою очередь, вызывает useEffectвызов и setSortedTodos.

Приведенный ниже пример работает именно так, как я хочу, однако eslint побуждает меня добавить todosк useEffectмассиву зависимостей, и если я это сделаю, это вызовет бесконечный цикл (как и следовало ожидать).

const [todos, setTodos] = useState([]);
const [sortKey, setSortKey] = useState('title');

const setSortedTodos = useCallback((data) => {
  const cloned = data.slice(0);

  const sorted = cloned.sort((a, b) => {
    const v1 = a[sortKey].toLowerCase();
    const v2 = b[sortKey].toLowerCase();

    if (v1 < v2) {
      return -1;
    }

    if (v1 > v2) {
      return 1;
    }

    return 0;
  });

  setTodos(sorted);
}, [sortKey]);

useEffect(() => {
    setSortedTodos(todos);
}, [setSortedTodos]);

Живой пример:

Я думаю, что должен быть лучший способ сделать это, чтобы Эслинт был счастлив.

DanV
источник
1
Просто примечание: sortобратный вызов может быть простым: return a[sortKey].toLowerCase().localeCompare(b[sortKey].toLowerCase());что также имеет преимущество в сравнении локали, если среда имеет разумную информацию о локали. Если хотите, вы также можете применить к нему деструктуризацию: pastebin.com/7X4M1XTH
TJ Crowder
Какую ошибку eslintвыкидывает?
Luze
Не могли бы вы обновить вопрос, чтобы предоставить работоспособный минимальный воспроизводимый пример проблемы с использованием фрагментов стека ( [<>]кнопка панели инструментов)? Фрагменты стека поддерживают React, включая JSX; вот как это сделать . Таким образом, люди могут проверить, что в их предложенных решениях нет проблемы бесконечного цикла ...
TJ Crowder
Это действительно интересный подход и действительно интересная проблема. Как вы говорите, вы можете понять, почему ESLint считает, что вам нужно добавить todosмассив зависимостей useEffect, и вы можете понять, почему этого не следует делать . :-)
TJ Crowder
Я добавил живой пример для вас. Очень хочу увидеть это ответ.
TJ Crowder

Ответы:

8

Я бы сказал, что это означает, что идти по этому пути не идеально. Функция действительно зависит от todos. Если setTodosвызывается где-то еще, функция обратного вызова должна быть пересчитана, иначе она работает с устаревшими данными.

Почему вы храните отсортированный массив в любом случае? Вы можете использовать useMemoдля сортировки значений при изменении ключа или массива:

const sortedTodos = useMemo(() => {
  return Array.from(todos).sort((a, b) => {
    const v1 = a[sortKey].toLowerCase();
    const v2 = b[sortKey].toLowerCase();

    if (v1 < v2) {
      return -1;
    }

    if (v1 > v2) {
      return 1;
    }

    return 0;
  });
}, [sortKey, todos]);

Тогда ссылка sortedTodosвезде.

Живой пример:

Нет необходимости хранить отсортированные значения в состоянии, так как вы всегда можете получить / вычислить отсортированный массив из «базового» массива и ключа сортировки. Я бы сказал, что это также облегчает понимание вашего кода, поскольку он менее сложен.

Феликс Клинг
источник
О, приятное использование useMemo. Просто побочный вопрос, почему бы не использовать .localCompareв сортировке?
Tikkes
2
Это было бы действительно лучше. Я просто скопировал код ОП и не обратил на это внимания (не имеет отношения к проблеме).
Феликс Клинг
Действительно простое, понятное решение.
TJ Crowder
Ах да, используйте Memo! Забыл об этом :)
DanV
3

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

Зачем использовать эффект для клик-действия в любом случае? Вы можете запустить его в такой функции:

const [todos, setTodos] = useState([]);

function sortTodos(e) {
    const sortKey = e.target.value;
    const clonedTodos = [...todos];
    const sorted = clonedTodos.sort((a, b) => {
        return a[sortKey.toLowerCase()].localeCompare(b[sortKey.toLowerCase()]);
    });

    setTodos(sorted);
}

и в выпадающем списке сделайте onChange.

    <select onChange="sortTodos"> ......

Обратите внимание на зависимость, кстати, ESLint прав! Ваши Todos, в случае, описанном выше, являются зависимостью и должны быть в списке. Подход к выбору предмета неправильный, а значит и ваша проблема.

Tikkes
источник
2
"sort вернет новый экземпляр массива" Не встроенный метод сортировки. Сортирует массив на месте. data.slice(0)создает копию.
Феликс Клинг
Это неверно, когда вы делаете a, setStateпоскольку он не будет редактировать существующий объект и, следовательно, будет внутренне его клонировать. Неправильная формулировка в моем ответе, правда. Я отредактирую это.
Tikkes
setStateне клонирует данные. Почему вы так думаете?
Феликс Клинг
1
@ Tikkes - Нет, setStateничего не клонирует. Феликс и ОП верны, вам нужно скопировать массив перед сортировкой.
TJ Crowder
Хорошо да извините Кажется, мне нужно больше читать о внутренностях. Вы действительно должны копировать, а не изменять существующие state.
Tikkes
0

Что вам нужно сделать здесь, это использовать функциональную форму setState:

  const [todos, setTodos] = useState(exampleToDos);
    const [sortKey, setSortKey] = useState('title');

    const setSortedTodos = useCallback((data) => {

      setTodos(currTodos => {
        return currTodos.sort((a, b) => {
          const v1 = a[sortKey].toLowerCase();
          const v2 = b[sortKey].toLowerCase();

          if (v1 < v2) {
            return -1;
          }

          if (v1 > v2) {
            return 1;
          }

          return 0;
        });
      })

    }, [sortKey]);

    useEffect(() => {
        setSortedTodos(todos);
    }, [setSortedTodos, todos]);

Рабочие коды и коробка

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

Использование функционала setStateгарантирует, что вы получите последнее значение состояния и не измените первоначальное значение состояния.

ясность
источник
"и не изменяйте исходное значение состояния." Я не думаю, что React передает копию в обратный вызов. .sortизменяет массив на месте, так что вам все равно придется копировать его самостоятельно.
Феликс Клинг
Хм, хорошая мысль, я на самом деле не могу найти подтверждения, что он передает копию состояния, хотя я помню, что читал что-то подобное ранее.
Ясность
Нашел это здесь: Reactionjs.org/docs/… . «мы можем использовать функциональную форму обновления setState. Это позволяет нам указать, как нужно менять состояние, не ссылаясь на текущее состояние »
Clarity
Я не читаю это как получение копии. Это просто означает, что вам не нужно указывать текущее состояние как «внешнюю» зависимость.
Феликс Клинг