useEffect - Empêche la boucle infinie lors de la mise à jour de l'état

9

J'aimerais que l'utilisateur puisse trier une liste de tâches. Lorsque les utilisateurs sélectionnent un élément dans une liste déroulante, il définit le sortKeyqui créera une nouvelle version de setSortedTodos, et déclenchera à son tour l' useEffectappel et setSortedTodos.

L'exemple ci-dessous fonctionne exactement comme je le souhaite, mais eslint m'invite à ajouter todosau useEffecttableau de dépendance, et si je le fais, cela provoque une boucle infinie (comme vous vous en doutez).

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

Exemple en direct:

Je pense qu'il doit y avoir une meilleure façon de faire cela qui garde eslint heureux.

DanV
la source
1
Juste une remarque: le sortrappel peut être juste: return a[sortKey].toLowerCase().localeCompare(b[sortKey].toLowerCase());ce qui a également l'avantage de comparer les paramètres régionaux si l'environnement dispose d'informations régionales raisonnables. Si vous le souhaitez, vous pouvez également lancer la déstructuration: pastebin.com/7X4M1XTH
TJ Crowder
Quelle erreur eslintlance?
Luze
Pourriez-vous mettre à jour la question pour fournir un exemple reproductible minimal exécutable du problème en utilisant des extraits de pile (le [<>]bouton de la barre d'outils)? Les extraits de pile prennent en charge React, y compris JSX; voici comment en faire un . De cette façon, les gens peuvent vérifier que leurs solutions proposées n'ont pas le problème de boucle infinie ...
TJ Crowder
C'est une approche vraiment intéressante et un problème vraiment intéressant. Comme vous le dites, vous pouvez comprendre pourquoi ESLint pense que vous devez ajouter todosau tableau de dépendances useEffect, et vous pouvez voir pourquoi vous ne devriez pas. :-)
TJ Crowder
J'ai ajouté l'exemple en direct pour vous. Je veux vraiment voir cette réponse.
TJ Crowder

Réponses:

8

Je dirais que cela signifie que procéder ainsi n'est pas idéal. La fonction est en effet dépendante de todos. Si setTodosest appelé ailleurs, la fonction de rappel doit être recalculée, sinon elle fonctionne sur des données périmées.

Pourquoi stockez-vous le tableau trié dans l'état de toute façon? Vous pouvez utiliser useMemopour trier les valeurs lorsque la clé ou le tableau change:

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

Puis référence sortedTodospartout.

Exemple en direct:

Il n'est pas nécessaire de stocker les valeurs triées dans l'état, car vous pouvez toujours dériver / calculer le tableau trié à partir du tableau "de base" et de la clé de tri. Je dirais que cela rend également votre code plus facile à comprendre car il est moins complexe.

Felix Kling
la source
Oh bonne utilisation de useMemo. Juste une question secondaire, pourquoi ne pas utiliser .localComparedans le genre?
Tikkes
2
Ce serait en effet mieux. Je viens de copier le code de l'OP et je n'y ai pas prêté attention (pas vraiment pertinent pour le problème).
Felix Kling
Solution vraiment simple et facile à comprendre.
TJ Crowder
Ah oui, useMemo! J'ai oublié ça :)
DanV
3

La raison de la boucle infinie est que todos ne correspond pas à la référence précédente et que l'effet se réexécutera.

Pourquoi utiliser quand même un effet pour un clic? Vous pouvez l'exécuter dans une fonction comme celle-ci:

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

et sur votre liste déroulante, faites un onChange.

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

Remarque sur la dépendance d'ailleurs, ESLint a raison! Vos Todos, dans le cas décrit ci-dessus, sont une dépendance et doivent figurer dans la liste. L'approche sur la sélection d'un article est erronée, et donc votre problème.

Tikkes
la source
2
"sort renverra une nouvelle instance d'un tableau" Pas la méthode de tri intégrée. Il trie le tableau sur place. data.slice(0)crée la copie.
Felix Kling
Ce n'est pas vrai lorsque vous effectuez un, setStatecar il ne modifiera pas l'objet existant et le clonera donc en interne. Mauvais libellé dans ma réponse, vrai. Je vais éditer ça.
Tikkes
setStatene clone pas les données. Pourquoi pensez-vous cela?
Felix Kling
1
@Tikkes - Non, setStatene clone rien. Felix et l'OP sont corrects, vous devez copier le tableau avant de le trier.
TJ Crowder
D'accord oui, désolé. J'ai besoin de plus de lecture sur les internes, semble-t-il. Vous devez en effet copier et ne pas modifier l'existant state.
Tikkes
0

Ce que vous devez faire ici est d'utiliser une forme fonctionnelle de 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]);

Codes et boîte de travail

Même si vous copiez l'état afin de ne pas muter l'original, il n'est toujours pas garanti que vous obtiendrez sa dernière valeur, car le paramètre étant asynchrone. De plus, la plupart des méthodes renverront une copie superficielle, vous pourriez donc finir par muter l'état d'origine de toute façon.

L'utilisation de la fonctionnalité setStategarantit que vous obtenez la dernière valeur de l'état et que vous ne modifiez pas la valeur de l'état d'origine.

Clarté
la source
"et ne modifiez pas la valeur de l'état d'origine." Je ne pense pas que React passe une copie au rappel. .sortmodifie le tableau en place, vous devrez donc le copier vous-même.
Felix Kling
Hmm, bon point, je ne trouve en fait aucune confirmation qu'il passe la copie de l'état, même si je me souviens avoir lu smth comme ça plus tôt.
Clarity
Je l'ai trouvé ici: reactjs.org/docs/… . «nous pouvons utiliser la forme de mise à jour fonctionnelle de setState. Il nous permet de spécifier comment l'état doit changer sans référencer l'état actuel '
Clarity
Je ne vois pas cela comme une copie. Cela signifie simplement que vous n'avez pas à fournir l'état actuel en tant que dépendance "externe".
Felix Kling