Come confrontare oldValues ​​e newValues ​​su React Hooks useEffect?


151

Diciamo che ho 3 input: rate, sendAmount e receiveAmount. Ho messo 3 input su parametri differenti di EffectEffect. Le regole sono:

  • Se sendAmount è cambiato, calcolo receiveAmount = sendAmount * rate
  • Se changeAmount è cambiato, calcolo sendAmount = receiveAmount / rate
  • Se la tariffa è cambiata, calcolo receiveAmount = sendAmount * ratequando sendAmount > 0o calcolo sendAmount = receiveAmount / ratequandoreceiveAmount > 0

Ecco la codesandbox https://codesandbox.io/s/pkl6vn7x6j per dimostrare il problema.

C'è un modo per confrontare il oldValuese newValuessimili su componentDidUpdateinvece di creare 3 gestori per questo caso?

Grazie


Ecco la mia soluzione finale con usePrevious https://codesandbox.io/s/30n01w2r06

In questo caso, non posso usare più useEffectperché ogni modifica porta alla stessa chiamata di rete. Ecco perché uso anche changeCountper tenere traccia delle modifiche. Questo changeCountè utile anche per tenere traccia delle modifiche solo dal locale, quindi posso impedire chiamate di rete non necessarie a causa delle modifiche dal server.


In che modo dovrebbe essere d'aiuto componentDidUpdate? Dovrai comunque scrivere queste 3 condizioni.
Estus Flask,

Ho aggiunto una risposta con 2 soluzioni opzionali. Uno di loro funziona per te?
Ben Carp,

Risposte:


210

Puoi scrivere un hook personalizzato per fornirti un oggetto di scena precedente usando useRef

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

e poi usalo in 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])
}

Tuttavia, è sempre più chiaro e probabilmente migliore e più chiaro leggere e comprendere se si utilizzano due useEffectseparatamente per ogni ID di modifica che si desidera elaborare separatamente


8
Grazie per la nota sull'uso di due useEffectchiamate separatamente. Non sapevo che potresti usarlo più volte!
JasonH,

3
Ho provato il codice sopra, ma eslint mi sta avvertendo che useEffectmancano dipendenzeprevAmount
Littlee,

2
Poiché prevAmount contiene un valore per il valore precedente di state / props, non è necessario passarlo come dipendenza per usareEffect e puoi disabilitare questo avviso per il caso particolare. Puoi leggere questo post per maggiori dettagli?
Shubham Khatri,

Adoro questo - useRef viene sempre in soccorso.
Fernando Rojo,

@ShubhamKhatri: qual è la ragione per il wrapping useEffect di ref.current = value?
curly_brackets

40

Nel caso in cui qualcuno stia cercando una versione TypeScript di uso precedente:

import { useEffect, useRef } from "react";

const usePrevious = <T extends {}>(value: T): T | undefined => {
  const ref = useRef<T>();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
};

2
Si noti che questo non funzionerà in un file TSX, per farlo funzionare in una modifica del file TSX per const usePrevious = <T extends any>(...far sì che l'interprete veda che <T> non è JSX ed è una restrizione generica
apokryfos,

1
Puoi spiegare perché <T extends {}>non funzionerà. Sembra funzionare per me, ma sto cercando di capire la complessità di usarlo in quel modo.
Karthikeyan_kk,

.tsxi file contengono jsx che è pensato per essere identico a HTML. È possibile che il generico <T>sia un tag componente non chiuso.
SeedyROM

Che dire del tipo di reso? RicevoMissing return type on function.eslint(@typescript-eslint/explicit-function-return-type)
Saba Ahang il

@SabaAhang Siamo spiacenti! TS utilizzerà l'inferenza di tipo per gestire i tipi di restituzione per la maggior parte, ma se è stata abilitata la linting, ho aggiunto un tipo di restituzione per eliminare gli avvisi.
SeedyROM

25

Opzione 1: uso Confronta gancio

Il confronto tra un valore corrente e un valore precedente è un modello comune e giustifica un hook personalizzato che nasconde i dettagli di implementazione.

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

dimostrazione

Opzione 2: esegui useEffect quando il valore cambia

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

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

dimostrazione


La seconda opzione ha funzionato per me. Potete per favore guidarmi perché devo scrivere useEffect Twice?
Tarun Nagpal,

@TarunNagpal, non è necessario utilizzare useEffect due volte. Dipende dal tuo caso d'uso. Immagina di voler solo registrare quale val è cambiato. Se abbiamo sia val1 che val2 nel nostro array di dipendenze, verrà eseguito ogni volta che uno qualsiasi dei valori è cambiato. Quindi all'interno della funzione che passiamo dovremo capire quale val è stata modificata per registrare il messaggio corretto.
Ben Carp

Grazie per la risposta. Quando dici "all'interno della funzione che passiamo dovremo capire quale val è cambiata". Come possiamo raggiungerlo. Si prega di guidare
Tarun Nagpal il

6

Ho appena pubblicato il delta-reazione che risolve questo esatto tipo di scenario. Secondo me, useEffectha troppe responsabilità.

responsabilità

  1. Confronta tutti i valori nel suo array di dipendenze usando Object.is
  2. Esegue callback di effetti / cleanup in base al risultato di # 1

Responsabilità di rottura

react-deltarompe useEffectle responsabilità in diversi piccoli uncini.

Responsabilità n. 1

Responsabilità # 2

Nella mia esperienza, questo approccio è più flessibile, pulito e conciso di useEffect/ useRefsoluzioni.


Proprio quello che sto cercando. Lo proverò!
hackerl33t,

sembra davvero buono ma non è un po 'eccessivo?
Hisato il

@Hisato molto bene potrebbe essere eccessivo. È una specie di API sperimentale. E francamente non l'ho usato molto nei miei team perché non è ampiamente conosciuto o adottato. In teoria sembra un po 'carino, ma in pratica potrebbe non valerne la pena.
Austin Malerba,

3

Poiché lo stato non è strettamente associato all'istanza del componente nei componenti funzionali, non è possibile raggiungere lo stato precedente useEffectsenza prima salvarlo, ad esempio con useRef. Questo significa anche che l'aggiornamento dello stato potrebbe essere stato implementato in modo errato nel posto sbagliato perché lo stato precedente è disponibile all'interno della setStatefunzione di aggiornamento.

Questo è un buon caso d'uso per il useReducerquale fornisce un negozio simile a Redux e consente di implementare il rispettivo modello. Gli aggiornamenti di stato vengono eseguiti in modo esplicito, quindi non è necessario capire quale proprietà di stato viene aggiornata; questo è già chiaro dall'azione inviata.

Ecco un esempio di come potrebbe apparire:

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

Ciao @estus, grazie per questa idea. Mi dà un altro modo di pensare. Ma ho dimenticato di dire che devo chiamare l'API per ciascuno dei casi. Hai qualche soluzione per questo?
rwinzhang,

Cosa intendi? Inside handleChange? "API" significa endpoint API remoto? Puoi aggiornare i codici e la casella dalla risposta con i dettagli?
Estus Flask,

Dall'aggiornamento posso presumere che tu voglia recuperare in sendAmountmodo asincrono, ecc. Invece di calcolarli, giusto? Questo cambia molto. È possibile farlo con useReducema può essere difficile. Se hai avuto a che fare con Redux prima di allora potresti sapere che le operazioni asincrone non sono semplici lì. github.com/reduxjs/redux-thunk è un'estensione popolare per Redux che consente azioni asincrone. Ecco una demo che aumenta useReducer con lo stesso pattern (useThunkReducer), codesandbox.io/s/6z4r79ymwr . Si noti che la funzione di invio è async, è possibile effettuare richieste lì (commentate).
Estus Flask il

1
Il problema con la domanda è che hai fatto una domanda su una cosa diversa che non era il tuo caso reale e poi l'hai cambiata, quindi ora è una domanda molto diversa mentre le risposte esistenti appaiono come se avessero semplicemente ignorato la domanda. Questa non è una pratica raccomandata su SO perché non ti aiuta a ottenere la risposta che desideri. Per favore, non rimuovere parti importanti dalla domanda a cui fanno già riferimento le risposte (formule di calcolo). Se hai ancora problemi (dopo il mio commento precedente (probabilmente lo fai), considera di porre una nuova domanda che rifletta il tuo caso e il link a questo come il tuo precedente tentativo.
Estus Flask

Ciao estus, penso che questo codesandbox codesandbox.io/s/6z4r79ymwr non sia ancora aggiornato. Ritornerò indietro alla domanda, scusami per quello.
rwinzhang,

3

L'uso di Ref introdurrà un nuovo tipo di bug nell'app.

Vediamo questo caso usando usePreviousquello che qualcuno ha commentato prima:

  1. prop.minTime: 5 ==> ref.current = 5 | imposta ref.current
  2. prop.minTime: 5 ==> ref.current = 5 | il nuovo valore è uguale a ref.current
  3. prop.minTime: 8 ==> ref.current = 5 | il nuovo valore NON è uguale a ref.current
  4. prop.minTime: 5 ==> ref.current = 5 | il nuovo valore è uguale a ref.current

Come possiamo vedere qui, non stiamo aggiornando l'interno refperché stiamo usandouseEffect


1
React ha questo esempio ma usano il state... e non il props... quando ti preoccupi dei vecchi oggetti di scena, allora si verificherà un errore.
santomegonzalo,

3

Per un confronto di oggetti di scena davvero semplice puoi usare useEffect per verificare facilmente se un oggetto di scena è stato aggiornato.

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

}

useEffect eseguirà quindi il codice solo se il prop cambia.


1
Funziona solo una volta, dopo le propmodifiche consecutive non sarai in grado di farlo~ do stuff here ~
Karolis Šarapnickis,

2
Sembra che dovresti chiamare setHasPropChanged(false)alla fine di ~ do stuff here ~"resettare" il tuo stato. (Ma questo si ripristinerebbe in un nuovo rendering)
Kevin Wang

Grazie per il feedback, hai ragione entrambe, soluzione aggiornata
Charlie Tupman,

@ AntonioPavicevac-Ortiz Ho aggiornato la risposta per rendere ora il propHasChanged come vero, che poi lo chiamerebbe una volta sul rendering, potrebbe essere una soluzione migliore solo per strappare l'usoEffetto e controllare il prop
Charlie Tupman,

Penso che il mio uso originale di questo sia andato perso. Guardando indietro al codice puoi semplicemente usare useEffect
Charlie Tupman l'

2

Uscendo dalla risposta accettata, una soluzione alternativa che non richiede un hook personalizzato:

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

1
Buona soluzione, ma contiene errori di sintassi. useRefno userRef. Hai dimenticato di usare la correnteprevAmount.current
Blake Plumb il

0

Ecco un gancio personalizzato che uso e che ritengo sia più intuitivo dell'uso 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)
}

Utilizzeresti useTransitioncome segue.

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

Spero che aiuti.

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.