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кидання?
Люзе

Чи можете ви оновити питання, щоб надати мінімально відтворюваний приклад проблеми, який можна виконати, використовуючи фрагменти стека ( [<>]кнопка панелі інструментів)? Сніппети стека підтримують 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всюди.

Живий приклад:

Немає необхідності зберігати відсортовані значення у стані, оскільки ви завжди можете вивести / обчислити відсортований масив із масиву "base" та ключа сортування. Я стверджую, що це також полегшує розуміння вашого коду, оскільки він менш складний.


О приємне використання useMemo. Просто побічне запитання, чому б не використовувати .localCompareв сорту?
Тіккес

2
Це дійсно було б краще. Я просто скопіював код ОП і не звернув на це уваги (не дуже стосується проблеми).
Фелікс Клінг

Дійсно просте, легко зрозуміле рішення.
TJ Crowder

Ага так, користуйся пам'яткою! Забув про це :)
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 має рацію! Ваші Тодоси, у випадку, описаному вище, залежність і повинні бути у списку. Підхід до вибору предмета неправильний, а отже, і ваша проблема.


2
"sort поверне новий екземпляр масиву" Не вбудований метод сортування. Він сортує масив на місці. data.slice(0)створює копію.
Фелікс Клінг

Це неправда, коли ви робите це, setStateоскільки він не буде редагувати існуючий об'єкт, і він буде клонувати його внутрішньо. Неправильне формулювання у моїй відповіді, правда. Я це відредагую.
Тіккес

setStateне клонує дані. Чому ти так думаєш?
Фелікс Клінг

1
@Tikkes - Ні, setStateнічого не клонує . Фелікс і ОП є правильними, вам потрібно скопіювати масив, перш ніж сортувати його.
TJ Crowder

Гаразд, так, вибачте. Мені потрібно ще читання з питань внутрішніх справ, здається. Ви дійсно повинні копіювати, а не змінювати існуючі state.
Тіккес

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змінює масив на місці, тож вам доведеться копіювати його самостійно.
Фелікс Клінг

Хм, добре, я фактично не можу знайти жодного підтвердження того, що він передає копію стану, але я пам’ятаю, що читав щось подібне раніше.
Чіткість

Знайшов тут: reactjs.org/docs / ... . 'ми можемо використовувати функціональну форму оновлення setState. Це дозволяє нам уточнити, як потрібно змінити державу, не посилаючись на поточний стан '
Чіткість

Я не читаю це як отримання копії. Це просто означає, що вам не потрібно надавати поточний стан як "зовнішню" залежність.
Фелікс Клінг
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.