Comment comparer oldValues ​​et newValues ​​sur React Hooks useEffect?

151

Disons que j'ai 3 entrées: rate, sendAmount et receiveAmount. Je mets que 3 entrées sur useEffect des paramètres différents. Les règles sont:

  • Si sendAmount a changé, je calcule receiveAmount = sendAmount * rate
  • Si receiveAmount a changé, je calcule sendAmount = receiveAmount / rate
  • Si le taux a changé, je calcule receiveAmount = sendAmount * ratequand sendAmount > 0ou je calcule sendAmount = receiveAmount / ratequandreceiveAmount > 0

Voici le codeandbox https://codesandbox.io/s/pkl6vn7x6j pour illustrer le problème.

Existe-t-il un moyen de comparer les oldValueset les newValuesautres au componentDidUpdatelieu de créer 3 gestionnaires pour ce cas?

Merci


Voici ma solution finale avec usePrevious https://codesandbox.io/s/30n01w2r06

Dans ce cas, je ne peux pas utiliser plusieurs useEffectcar chaque changement conduit au même appel réseau. C'est pourquoi j'utilise également changeCountpour suivre le changement. Cela est changeCountégalement utile pour suivre les modifications à partir du local uniquement, afin que je puisse éviter les appels réseau inutiles en raison des modifications du serveur.

rwinzhang
la source
Comment componentDidUpdate est-il censé aider exactement? Vous devrez toujours écrire ces 3 conditions.
Estus Flask
J'ai ajouté une réponse avec 2 solutions optionnelles. L'un d'eux fonctionne-t-il pour vous?
Ben Carp

Réponses:

210

Vous pouvez écrire un hook personnalisé pour vous fournir des accessoires précédents en utilisant useRef

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

puis utilisez-le dans useEffect

const Component = (props) => {
    const {receiveAmount, sendAmount } = props
    const prevAmount = usePrevious({receiveAmount, sendAmount});
    useEffect(() => {
        if(prevAmount.receiveAmount !== receiveAmount) {

         // process here
        }
        if(prevAmount.sendAmount !== sendAmount) {

         // process here
        }
    }, [receiveAmount, sendAmount])
}

Cependant, il est plus clair et probablement meilleur et plus clair à lire et à comprendre si vous en utilisez deux useEffectséparément pour chaque identifiant de modification que vous souhaitez traiter séparément

Shubham Khatri
la source
8
Merci pour la note sur l'utilisation de deux useEffectappels séparément. Je ne savais pas que vous pouviez l'utiliser plusieurs fois!
JasonH
3
J'ai essayé le code ci-dessus, mais eslint m'avertit que useEffectles dépendances manquantesprevAmount
Littlee
2
Puisque prevAmount contient la valeur de la valeur précédente de state / props, vous n'avez pas besoin de la passer en tant que dépendance à useEffect et vous pouvez désactiver cet avertissement pour le cas particulier. Vous pouvez lire cet article pour plus de détails?
Shubham Khatri
J'adore ça - useRef vient toujours à la rescousse.
Fernando Rojo
@ShubhamKhatri: Quelle est la raison de l'encapsulation useEffect de ref.current = value?
curly_brackets
40

Dans le cas où quiconque recherche une version d'utilisation de TypeScript

import { useEffect, useRef } from "react";

const usePrevious = <T extends {}>(value: T): T | undefined => {
  const ref = useRef<T>();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
};
SeedyROM
la source
2
Notez que cela ne fonctionnera pas dans un fichier TSX, pour que cela fonctionne dans un fichier TSX, changez pour const usePrevious = <T extends any>(...faire en sorte que l'interpréteur voit que <T> n'est pas JSX et est une restriction générique
apokryfos
1
Pouvez-vous expliquer pourquoi <T extends {}>ne fonctionnera pas. Cela semble fonctionner pour moi, mais j'essaie de comprendre la complexité de l'utiliser comme ça.
Karthikeyan_kk
.tsxles fichiers contiennent jsx qui est censé être identique à html. Il est possible que le générique <T>soit une balise de composant non fermée.
SeedyROM
Qu'en est-il du type de retour? Je reçoisMissing return type on function.eslint(@typescript-eslint/explicit-function-return-type)
Saba Ahang le
@SabaAhang Désolé! TS utilisera l'inférence de type pour gérer les types de retour pour la plupart, mais si vous avez activé le linting, j'ai ajouté un type de retour pour supprimer les avertissements.
SeedyROM
25

Option 1 - crochet useCompare

La comparaison d'une valeur actuelle à une valeur précédente est un modèle courant et justifie un hook personnalisé qui cache les détails de l'implémentation.

const useCompare = (val: any) => {
    const prevVal = usePrevious(val)
    return prevVal !== val
}

const usePrevious = (value) => {
    const ref = useRef();
    useEffect(() => {
      ref.current = value;
    });
    return ref.current;
}

const Component = (props) => {
  ...
  const hasVal1Changed = useCompare(val1)
  const hasVal2Changed = useCompare(val2);
  useEffect(() => {
    if (hasVal1Changed ) {
      console.log("val1 has changed");
    }
    if (hasVal2Changed ) {
      console.log("val2 has changed");
    }
  });

  return <div>...</div>;
};

Démo

Option 2 - Exécutez useEffect lorsque la valeur change

const Component = (props) => {
  ...
  useEffect(() => {
    console.log("val1 has changed");
  }, [val1]);
  useEffect(() => {
    console.log("val2 has changed");
  }, [val2]);

  return <div>...</div>;
};

Démo

Ben Carp
la source
La deuxième option a fonctionné pour moi. Pouvez-vous me dire pourquoi je dois écrire useEffect Twice?
Tarun Nagpal
@TarunNagpal, vous n'avez pas nécessairement besoin d'utiliser useEffect deux fois. Cela dépend de votre cas d'utilisation. Imaginez que nous voulons simplement enregistrer quel val a changé. Si nous avons à la fois val1 et val2 dans notre tableau de dépendances, il s'exécutera chaque fois qu'une des valeurs a changé. Ensuite, à l'intérieur de la fonction que nous passons, nous devrons déterminer quelle valeur a changé pour enregistrer le message correct.
Ben Carp
Merci pour la réponse. Quand vous dites "à l'intérieur de la fonction que nous passons, nous devrons déterminer quel val a changé". Comment y parvenir. S'il vous plaît guide
Tarun Nagpal
6

Je viens de publier react-delta qui résout ce genre de scénario exact. À mon avis, useEffecta trop de responsabilités.

Responsabilités

  1. Il compare toutes les valeurs de son tableau de dépendances en utilisant Object.is
  2. Il exécute des rappels d'effet / nettoyage basés sur le résultat de # 1

Rompre les responsabilités

react-deltadivise useEffectles responsabilités en plusieurs petits crochets.

Responsabilité # 1

Responsabilité # 2

D'après mon expérience, cette approche est plus flexible, propre et concise que useEffect/ useRefsolutions.

Austin Malerba
la source
Juste ce que je recherche. Je vais l'essayer!
hackerl33t
semble vraiment bon mais n'est-ce pas un peu exagéré?
Hisato
@Hisato, cela pourrait très bien être exagéré. C'est en quelque sorte une API expérimentale. Et franchement, je ne l'ai pas beaucoup utilisé au sein de mes équipes car il n'est pas largement connu ou adopté. En théorie, cela semble plutôt sympa, mais en pratique, cela ne vaut peut-être pas la peine.
Austin Malerba
3

Étant donné que l'état n'est pas étroitement associé à l'instance de composant dans les composants fonctionnels, l'état précédent ne peut pas être atteint useEffectsans l'enregistrer au préalable, par exemple avec useRef. Cela signifie également que la mise à jour de l'état a peut-être été mal implémentée au mauvais endroit car l'état précédent est disponible dans la setStatefonction de mise à jour.

C'est un bon cas d'utilisation pour useReducerlequel fournit un stockage de type Redux et permet d'implémenter le modèle respectif. Les mises à jour d'état sont effectuées explicitement, il n'est donc pas nécessaire de déterminer quelle propriété d'état est mise à jour; cela ressort déjà clairement de l'action expédiée.

Voici un exemple à quoi cela peut ressembler:

function reducer({ sendAmount, receiveAmount, rate }, action) {
  switch (action.type) {
    case "sendAmount":
      sendAmount = action.payload;
      return {
        sendAmount,
        receiveAmount: sendAmount * rate,
        rate
      };
    case "receiveAmount":
      receiveAmount = action.payload;
      return {
        sendAmount: receiveAmount / rate,
        receiveAmount,
        rate
      };
    case "rate":
      rate = action.payload;
      return {
        sendAmount: receiveAmount ? receiveAmount / rate : sendAmount,
        receiveAmount: sendAmount ? sendAmount * rate : receiveAmount,
        rate
      };
    default:
      throw new Error();
  }
}

function handleChange(e) {
  const { name, value } = e.target;
  dispatch({
    type: name,
    payload: value
  });
}

...
const [state, dispatch] = useReducer(reducer, {
  rate: 2,
  sendAmount: 0,
  receiveAmount: 0
});
...
Fiole d'Estus
la source
Salut @estus, merci pour cette idée. Cela me donne une autre façon de penser. Mais, j'ai oublié de mentionner que je dois appeler l'API pour chacun des cas. Avez-vous une solution pour cela?
rwinzhang
Que voulez-vous dire? Poignée intérieureChange? «API» signifie-t-il un point de terminaison d'API distant? Pouvez-vous mettre à jour les codes et la boîte à partir de la réponse avec des détails?
Estus Flask
À partir de la mise à jour, je peux supposer que vous souhaitez récupérer sendAmount, etc. de manière asynchrone au lieu de les calculer, non? Cela change beaucoup. Il est possible de le faire avec, useReducemais cela peut être délicat. Si vous avez déjà traité avec Redux, vous savez peut-être que les opérations asynchrones ne sont pas simples. github.com/reduxjs/redux-thunk est une extension populaire pour Redux qui permet des actions asynchrones. Voici une démo qui augmente useReducer avec le même modèle (useThunkReducer), codesandbox.io/s/6z4r79ymwr . Notez que la fonction distribuée est async, vous pouvez y faire des requêtes (commentées).
Estus Flask
1
Le problème avec la question est que vous avez posé une question sur une chose différente qui n'était pas votre cas réel, puis que vous l'avez modifiée, alors maintenant c'est une question très différente alors que les réponses existantes semblent simplement ignorer la question. Ce n'est pas une pratique recommandée sur SO car cela ne vous aide pas à obtenir la réponse que vous souhaitez. Veuillez ne pas supprimer les parties importantes de la question auxquelles les réponses font déjà référence (formules de calcul). Si vous rencontrez toujours des problèmes (après mon commentaire précédent (vous le faites probablement), envisagez de poser une nouvelle question qui reflète votre cas et établissez un lien vers celui-ci comme votre tentative précédente.
Estus Flask
Salut estus, je pense que ce codesandbox codesandbox.io/s/6z4r79ymwr n'est pas encore mis à jour. Je vais revenir sur la question, désolé pour cela.
rwinzhang
3

L'utilisation de Ref introduira un nouveau type de bogue dans l'application.

Voyons ce cas en utilisant ce usePreviousque quelqu'un a commenté auparavant:

  1. prop.minTime: 5 ==> ref.current = 5 | régler le courant de référence
  2. prop.minTime: 5 ==> ref.current = 5 | la nouvelle valeur est égale au courant de référence
  3. prop.minTime: 8 ==> ref.current = 5 | la nouvelle valeur n'est PAS égale au courant de référence
  4. prop.minTime: 5 ==> ref.current = 5 | la nouvelle valeur est égale au courant de référence

Comme nous pouvons le voir ici, nous ne mettons pas à jour l'interne refcar nous utilisonsuseEffect

santomegonzalo
la source
1
React a cet exemple, mais ils utilisent le state... et non le props... lorsque vous vous souciez des anciens accessoires, une erreur se produira.
santomegonzalo
3

Pour une comparaison d'accessoires vraiment simple, vous pouvez utiliser useEffect pour vérifier facilement si un accessoire a été mis à jour.

const myComponent = ({ prop }) => {
  useEffect(() => {
     ---Do stuffhere----
    }
  }, [prop])

}

useEffect n'exécutera alors votre code que si le prop change.

Charlie Tupman
la source
1
Cela ne fonctionne qu'une seule fois, après des propchangements consécutifs , vous ne pourrez pas~ do stuff here ~
Karolis Šarapnickis
2
On dirait que vous devriez appeler setHasPropChanged(false)à la fin de ~ do stuff here ~pour "réinitialiser" votre état. (Mais cela réinitialiserait dans un rendu supplémentaire)
Kevin Wang
Merci pour les commentaires, vous avez tous les deux raison, solution mise à jour
Charlie Tupman
@ AntonioPavicevac-Ortiz J'ai mis à jour la réponse pour rendre maintenant le propHasChanged comme vrai, ce qui l'appellerait une fois lors du rendu, pourrait être une meilleure solution juste pour déchirer le useEffect et vérifier simplement l'accessoire
Charlie Tupman
Je pense que mon utilisation initiale de ceci a été perdue. En regardant le code, vous pouvez simplement utiliser useEffect
Charlie Tupman
2

En partant de la réponse acceptée, une solution alternative qui ne nécessite pas de hook personnalisé:

const Component = (props) => {
  const { receiveAmount, sendAmount } = props;
  const prevAmount = useRef({ receiveAmount, sendAmount }).current;
  useEffect(() => {
    if (prevAmount.receiveAmount !== receiveAmount) {
     // process here
    }
    if (prevAmount.sendAmount !== sendAmount) {
     // process here
    }
    return () => { 
      prevAmount.receiveAmount = receiveAmount;
      prevAmount.sendAmount = sendAmount;
    };
  }, [receiveAmount, sendAmount]);
};
Drazen Bjelovuk
la source
1
Bonne solution, mais contient des erreurs de syntaxe. useRefnon userRef. J'ai oublié d'utiliser le courantprevAmount.current
Blake Plumb
0

Voici un crochet personnalisé que j'utilise et qui, à mon avis, est plus intuitif que de l'utiliser usePrevious.

import { useRef, useEffect } from 'react'

// useTransition :: Array a => (a -> Void, a) -> Void
//                              |_______|  |
//                                  |      |
//                              callback  deps
//
// The useTransition hook is similar to the useEffect hook. It requires
// a callback function and an array of dependencies. Unlike the useEffect
// hook, the callback function is only called when the dependencies change.
// Hence, it's not called when the component mounts because there is no change
// in the dependencies. The callback function is supplied the previous array of
// dependencies which it can use to perform transition-based effects.
const useTransition = (callback, deps) => {
  const func = useRef(null)

  useEffect(() => {
    func.current = callback
  }, [callback])

  const args = useRef(null)

  useEffect(() => {
    if (args.current !== null) func.current(...args.current)
    args.current = deps
  }, deps)
}

Vous utiliseriez useTransitioncomme suit.

useTransition((prevRate, prevSendAmount, prevReceiveAmount) => {
  if (sendAmount !== prevSendAmount || rate !== prevRate && sendAmount > 0) {
    const newReceiveAmount = sendAmount * rate
    // do something
  } else {
    const newSendAmount = receiveAmount / rate
    // do something
  }
}, [rate, sendAmount, receiveAmount])

J'espère que cela pourra aider.

Aadit M Shah
la source