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]);

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

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


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, в случае, описанном выше, являются зависимостью и должны быть в списке. Подход к выбору предмета неправильный, а значит и ваша проблема.


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

Я не читаю это как получение копии. Это просто означает, что вам не нужно указывать текущее состояние как «внешнюю» зависимость.
— Феликс Клинг
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.