useEffect: impedisce il ciclo infinito durante l'aggiornamento dello stato


9

Vorrei che l'utente fosse in grado di ordinare un elenco di cose da fare. Quando gli utenti selezionano un elemento da un menu a discesa, imposterà quello sortKeyche creerà una nuova versione setSortedTodose, a sua volta, attiverà useEffecte chiamerà setSortedTodos.

L'esempio seguente funziona esattamente come voglio, tuttavia eslint mi sta chiedendo di aggiungere todosl' useEffectarray di dipendenze e, se lo faccio, provoca un ciclo infinito (come ci si aspetterebbe).

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

Esempio live:

Sto pensando che ci debba essere un modo migliore per farlo che renda felice la fuga.


1
Solo una nota a sortmargine : il callback può essere solo: il return a[sortKey].toLowerCase().localeCompare(b[sortKey].toLowerCase());che ha anche il vantaggio di fare un confronto delle impostazioni locali se l'ambiente ha informazioni sulle impostazioni locali ragionevoli. Se lo desideri, puoi anche lanciarlo in modo distruttivo: pastebin.com/7X4M1XTH
TJ Crowder

Quale errore sta eslintlanciando?
Luze,

Potresti aggiornare la domanda per fornire un esempio riproducibile minimo riproducibile del problema usando Stack Snippet (il [<>]pulsante della barra degli strumenti)? Stack Snippet supportano React, incluso JSX; ecco come fare uno . In questo modo le persone possono verificare che le soluzioni proposte non presentino il problema del loop infinito ...
TJ Crowder,

Questo è un approccio davvero interessante e un problema davvero interessante. Come dici tu, puoi capire perché ESLint pensa di dover aggiungere todosl'array di dipendenze useEffecte puoi capire perché non dovresti. :-)
TJ Crowder,

Ho aggiunto l'esempio dal vivo per te. Voglio davvero vedere questa risposta.
TJ Crowder,

Risposte:


8

Direi che questo significa che andare in questo modo non è l'ideale. La funzione dipende infatti da todos. Se setTodosviene chiamato altrove, la funzione di callback deve essere ricalcolata, altrimenti opera su dati non aggiornati.

Perché comunque memorizzi l'array ordinato nello stato? È possibile utilizzare useMemoper ordinare i valori quando cambia la chiave o l'array:

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

Quindi riferimento sortedTodosovunque.

Esempio live:

Non è necessario memorizzare i valori ordinati nello stato, poiché è sempre possibile derivare / calcolare l'array ordinato dall'array "base" e dalla chiave di ordinamento. Direi che semplifica anche la comprensione del codice poiché è meno complesso.


Oh buon uso di useMemo. Solo una domanda a margine, perché non usarlo .localComparenel tipo?
Tikkes,

2
Sarebbe davvero meglio. Ho appena copiato il codice del PO e non ho prestato attenzione a questo (non pertinente al problema).
Felix Kling,

Soluzione davvero semplice, di facile comprensione.
TJ Crowder,

Ah sì, usa Memo! Dimenticato :)
DanV,

3

Il motivo del ciclo infinito è perché todos non corrisponde al riferimento precedente e l'effetto verrà eseguito nuovamente.

Perché usare comunque un effetto per un'azione a scatto? Puoi eseguirlo in una funzione come questa:

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

e sul tuo menu a discesa fai un onChange.

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

Nota sulla dipendenza tra l'altro, ESLint ha ragione! Il tuo Todos, nel caso sopra descritto, è una dipendenza e dovrebbe essere nell'elenco. L'approccio alla selezione di un articolo è sbagliato e quindi il tuo problema.


2
"sort restituirà una nuova istanza di un array" Non il metodo di ordinamento incorporato. Ordina l'array sul posto. data.slice(0)crea la copia.
Felix Kling,

Questo non è vero quando si esegue un setStatepoiché non modificherà l'oggetto esistente e quindi lo clonerà internamente. Espressione errata nella mia risposta, vero. Lo modificherò.
Tikkes,

setStatenon clona i dati. Perché pensi che?
Felix Kling,

1
@Tikkes - No, setStatenon clona nulla. Felix e l'OP sono corretti, è necessario copiare l'array prima di ordinarlo.
TJ Crowder,

Va bene si, scusa. Ho bisogno di un po 'più di lettura sugli interni, a quanto pare. Devi davvero copiare e non cambiare quello esistente state.
Tikkes,

0

Quello che devi fare qui è usare la forma funzionale di 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]);

Codici funzionanti e casella

Anche se stai copiando lo stato per non mutare quello originale, non è ancora garantito che otterrai il suo ultimo valore, poiché l'impostazione dello stato è asincrona. Inoltre la maggior parte dei metodi restituirà una copia superficiale, quindi potresti finire per mutare lo stato originale comunque.

L'uso di funzionale setStategarantisce di ottenere il valore più recente dello stato e di non modificare il valore dello stato originale.


"e non mutare il valore di stato originale." Non credo che React ne passi una copia al callback. .sortcambia l'array sul posto, quindi dovresti comunque copiarlo da solo.
Felix Kling,

Hmm, buon punto, in realtà non riesco a trovare alcuna conferma che passi la copia dello stato, anche se ricordo di aver letto smth in precedenza.
Chiarezza il

L'ho trovato qui: reazionejs.org/docs/… . 'possiamo usare il modulo di aggiornamento funzionale di setState. Ci consente di specificare come lo stato deve cambiare senza fare riferimento allo stato corrente "
Chiarezza il

Non lo leggo come ottenere una copia. Significa solo che non è necessario fornire lo stato corrente come dipendenza "esterna".
Felix Kling,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.