useEffect - Zapobiega nieskończonej pętli podczas aktualizacji stanu

9

Chciałbym, aby użytkownik mógł posortować listę rzeczy do zrobienia. Gdy użytkownicy wybiorą element z listy rozwijanej, ustawi, sortKeyktóra utworzy nową wersję setSortedTodos, a następnie uruchomi useEffecti zadzwonisetSortedTodos .

Poniższy przykład działa dokładnie tak, jak chcę, jednak eslint zachęca mnie do dodania todosdo useEffecttablicy zależności, a jeśli to zrobię, spowoduje nieskończoną pętlę (jak można się spodziewać).

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

Przykład na żywo:

Myślę, że musi istnieć lepszy sposób na zrobienie tego, co zapewni zadowolenie eslintowi.

DanV
źródło
1
Uwaga dodatkowa: sortwywołanie zwrotne może być po prostu: return a[sortKey].toLowerCase().localeCompare(b[sortKey].toLowerCase());co ma tę zaletę, że porównuje ustawienia narodowe, jeśli środowisko ma rozsądne informacje o ustawieniach regionalnych. Jeśli chcesz, możesz też rzucić w to destrukcją: pastebin.com/7X4M1XTH
TJ Crowder
Jaki błąd jest eslintgenerowany?
Luze,
Czy możesz zaktualizować pytanie, aby uzyskać działający minimalny odtwarzalny przykład problemu przy użyciu fragmentów stosu ( [<>]przycisk paska narzędzi)? Fragmenty stosu obsługują React, w tym JSX; oto jak to zrobić . W ten sposób ludzie mogą sprawdzić, czy ich proponowane rozwiązania nie mają problemu z nieskończoną pętlą ...
TJ Crowder
To naprawdę ciekawe podejście i naprawdę interesujący problem. Jak mówisz, możesz zrozumieć, dlaczego ESLint uważa, że ​​musisz dodać todosdo tablicy zależności useEffect, i możesz zobaczyć, dlaczego nie powinieneś. :-)
TJ Crowder
Dodałem dla ciebie przykład na żywo. Naprawdę chcę zobaczyć odpowiedź.
TJ Crowder

Odpowiedzi:

8

Twierdzę, że oznacza to, że takie podejście nie jest idealne. Funkcja jest rzeczywiście zależna od todos. GdybysetTodos zostanie wywołany w innym miejscu, funkcja wywołania zwrotnego musi zostać ponownie obliczona, w przeciwnym razie działa na nieaktualnych danych.

Dlaczego mimo wszystko przechowujesz posortowaną tablicę w stanie? Możesz użyć useMemodo sortowania wartości, gdy zmienia się klucz lub tablica:

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

Następnie odniesienie sortedTodos wszędzie.

Przykład na żywo:

Nie ma potrzeby przechowywania posortowanych wartości w stanie, ponieważ zawsze można wyprowadzić / obliczyć posortowaną tablicę z tablicy „podstawowej” i klucza sortowania. Twierdzę, że ułatwia to również zrozumienie kodu, ponieważ jest mniej skomplikowany.

Felix Kling
źródło
Och, niezłe użycie useMemo. Tylko pytanie poboczne, dlaczego nie użyć .localComparetego rodzaju?
Tikkes,
2
To byłoby rzeczywiście lepsze. Właśnie skopiowałem kod OP i nie zwróciłem na to uwagi (niezbyt istotne dla problemu).
Felix Kling
Naprawdę proste, łatwe do zrozumienia rozwiązanie.
TJ Crowder,
Ach tak, użyjMemo! Zapomniałem o tym :)
DanV
3

Powodem nieskończonej pętli jest to, że todos nie pasują do poprzedniego odwołania i efekt zostanie uruchomiony ponownie.

Po co w ogóle korzystać z efektu kliknięcia? Możesz uruchomić go w takiej funkcji:

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

a po rozwijaniu wykonaj onChange.

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

Przy okazji, zwróć uwagę na zależność, ESLint ma rację! Twoje zadania do wykonania, w przypadku opisanym powyżej, są zależne i powinny znajdować się na liście. Podejście do wyboru przedmiotu jest niewłaściwe, a zatem twój problem.

Tikkes
źródło
2
„sort zwróci nową instancję tablicy”, a nie wbudowana metoda sortowania. Sortuje tablicę w miejscu. data.slice(0)tworzy kopię.
Felix Kling
Nie jest to prawdą, kiedy to zrobisz, setStateponieważ nie będzie edytować istniejącego obiektu, a zatem sklonuje go wewnętrznie. Nieprawidłowe sformułowanie w mojej odpowiedzi, prawda. Zmienię to.
Tikkes,
setStatenie klonuje danych. Dlaczego myślisz, że?
Felix Kling
1
@Tikkes - Nie, setStateniczego nie klonuje . Felix i OP mają rację, musisz skopiować tablicę przed posortowaniem.
TJ Crowder
Okej, przepraszam. Wydaje mi się, że potrzebuję trochę więcej informacji na temat elementów wewnętrznych. Rzeczywiście musisz skopiować i nie zmieniać istniejącego state.
Tikkes,
0

Musisz tutaj użyć funkcjonalnej formy 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]);

Działające kody i skrzynka

Nawet jeśli kopiujesz stan, aby nie mutować oryginalnego, nadal nie ma gwarancji, że uzyskasz jego najnowszą wartość, ponieważ ustawienie stanu jest asynchroniczne. Ponadto większość metod zwróci płytką kopię, więc i tak może dojść do mutacji stanu oryginalnego.

Korzystanie z funkcji setStatezapewnia, że ​​otrzymujesz najnowszą wartość stanu i nie mutujesz oryginalnej wartości stanu.

Przejrzystość
źródło
„i nie mutuj oryginalnej wartości stanu”. Nie sądzę, aby React przekazał kopię do wywołania zwrotnego. .sortzmienia tablicę w miejscu, więc nadal musisz ją skopiować samodzielnie.
Felix Kling
Hmm, dobra uwaga, właściwie nie mogę znaleźć żadnego potwierdzenia, że ​​przekazuje kopię stanu, choć pamiętam, że czytałem coś takiego wcześniej.
Clarity
Znalazłem to tutaj: reagjs.org/docs/… . „Możemy użyć funkcjonalnej formy aktualizacji setState. Pozwala nam określić, w jaki sposób stan powinien się zmienić bez odwoływania się do bieżącego stanu
Clarity,
Nie czytam tego jako otrzymania kopii. Oznacza to po prostu, że nie musisz podawać bieżącego stanu jako zależności „zewnętrznej”.
Felix Kling