Wie vergleiche ich alte und neue Werte auf React Hooks useEffect?

149

Angenommen, ich habe drei Eingaben: rate, sendAmount und receiveAmount. Ich habe diese 3 Eingänge auf useEffect diffing params gesetzt. Die Regeln sind:

  • Wenn sendAmount geändert wurde, berechne ich receiveAmount = sendAmount * rate
  • Wenn receiveAmount geändert wurde, berechne ich sendAmount = receiveAmount / rate
  • Wenn sich die Rate ändert, berechne ich receiveAmount = sendAmount * ratewann sendAmount > 0oder sendAmount = receiveAmount / ratewannreceiveAmount > 0

Hier ist die Codesandbox https://codesandbox.io/s/pkl6vn7x6j , um das Problem zu demonstrieren.

Gibt es eine Möglichkeit, das oldValuesund newValuesdergleichen zu vergleichen, componentDidUpdateanstatt 3 Handler für diesen Fall zu erstellen?

Vielen Dank


Hier ist meine endgültige Lösung mit usePrevious https://codesandbox.io/s/30n01w2r06

In diesem Fall kann ich nicht mehrere verwenden, useEffectda jede Änderung zu demselben Netzwerkanruf führt. Deshalb changeCountverfolge ich die Änderung auch. Dies ist changeCountauch hilfreich, um Änderungen nur von lokal zu verfolgen, damit ich unnötige Netzwerkanrufe aufgrund von Änderungen vom Server verhindern kann.

rwinzhang
quelle
Wie genau soll componentDidUpdate helfen? Sie müssen diese 3 Bedingungen noch schreiben.
Estus Flask
Ich habe eine Antwort mit 2 optionalen Lösungen hinzugefügt. Arbeitet einer von ihnen für Sie?
Ben Carp

Antworten:

207

Mit useRef können Sie einen benutzerdefinierten Hook schreiben, um frühere Requisiten bereitzustellen

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

und dann verwenden Sie es 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])
}

Es ist jedoch klarer und wahrscheinlich besser und klarer zu lesen und zu verstehen, wenn Sie zwei useEffectseparat für jede Änderungs-ID verwenden, die Sie separat verarbeiten möchten

Shubham Khatri
quelle
8
Vielen Dank für den Hinweis, zwei useEffectAnrufe getrennt zu verwenden. Wusste nicht, dass du es mehrmals benutzen kannst!
JasonH
3
Ich habe den obigen Code ausprobiert, aber eslint warnt mich vor useEffectfehlenden AbhängigkeitenprevAmount
Littlee
2
Da prevAmount den Wert für den vorherigen Wert von state / props enthält, müssen Sie ihn nicht als Abhängigkeit für die Verwendung von Effect übergeben, und Sie können diese Warnung für den jeweiligen Fall deaktivieren. Sie können diesen Beitrag für weitere Details lesen?
Shubham Khatri
Ich liebe das - useRef kommt immer zur Rettung.
Fernando Rojo
@ShubhamKhatri: Was ist der Grund für den useEffect-Wrapping von ref.current = value?
curly_brackets
40

Falls jemand nach einer TypeScript-Version von usePrevious sucht:

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
quelle
2
Beachten Sie, dass dies in einer TSX-Datei nicht funktioniert. Damit dies in einer TSX-Datei funktioniert, ändern Sie dies, const usePrevious = <T extends any>(...damit der Interpreter denkt, dass <T> nicht JSX ist und eine generische Einschränkung darstellt
apokryfos
1
Können Sie erklären, warum <T extends {}>nicht funktioniert. Es scheint für mich zu funktionieren, aber ich versuche zu verstehen, wie komplex es ist, es so zu verwenden.
Karthikeyan_kk
.tsxDateien enthalten jsx, das mit HTML identisch sein soll. Es ist möglich, dass das Generikum <T>ein nicht geschlossenes Komponenten-Tag ist.
SeedyROM
Was ist mit dem Rückgabetyp? Ich bekommeMissing return type on function.eslint(@typescript-eslint/explicit-function-return-type)
Saba Ahang
@ SabaAhang Entschuldigung! TS verwendet die Typinferenz zum größten Teil, um Rückgabetypen zu verarbeiten. Wenn Sie jedoch Flusen aktiviert haben, habe ich einen Rückgabetyp hinzugefügt, um die Warnungen zu entfernen.
SeedyROM
24

Option 1 - useCompare-Hook

Das Vergleichen eines aktuellen Werts mit einem vorherigen Wert ist ein gängiges Muster und rechtfertigt einen eigenen benutzerdefinierten Hook, der Implementierungsdetails verbirgt.

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

Demo

Option 2 - Verwenden Sie useEffect, wenn sich der Wert ändert

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

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

Demo

Ben Carp
quelle
Die zweite Option hat bei mir funktioniert. Können Sie mir bitte erklären, warum ich useEffect Twice schreiben muss?
Tarun Nagpal
@TarunNagpal, Sie müssen useEffect nicht unbedingt zweimal verwenden. Dies hängt von Ihrem Anwendungsfall ab. Stellen Sie sich vor, wir möchten nur protokollieren, welcher Wert sich geändert hat. Wenn wir sowohl val1 als auch val2 in unserem Array von Abhängigkeiten haben, wird es jedes Mal ausgeführt, wenn sich einer der Werte geändert hat. Dann müssen wir innerhalb der Funktion, die wir übergeben, herausfinden, welcher Wert sich geändert hat, um die richtige Nachricht zu protokollieren.
Ben Carp
Danke für die Antwort. Wenn Sie sagen "innerhalb der Funktion, die wir übergeben, müssen wir herausfinden, welcher Wert sich geändert hat". Wie wir es erreichen können. Bitte führen
Tarun Nagpal
6

Ich habe gerade React-Delta veröffentlicht , das genau diese Art von Szenario löst. Meiner Meinung nach,useEffect hat zu viele Verantwortlichkeiten.

Verantwortlichkeiten

  1. Es vergleicht alle Werte in seinem Abhängigkeitsarray mit Object.is
  2. Es werden Effekt- / Bereinigungsrückrufe basierend auf dem Ergebnis von # 1 ausgeführt

Verantwortlichkeiten auflösen

react-deltabricht useEffectdie Verantwortung in mehrere kleinere Haken.

Verantwortung # 1

Verantwortung # 2

Nach meiner Erfahrung ist dieser Ansatz flexibler, sauberer und prägnanter als useEffect/ useRefLösungen.

Austin Malerba
quelle
Genau das, wonach ich suche. Ich werde es ausprobieren!
hackerl33t
sieht wirklich gut aus, aber ist es nicht ein bisschen übertrieben?
Hisato
@ Hisato es könnte sehr gut übertrieben sein. Es ist so etwas wie eine experimentelle API. Und ehrlich gesagt habe ich es in meinen Teams nicht viel benutzt, weil es nicht allgemein bekannt ist oder angenommen wird. Theoretisch klingt es irgendwie nett, aber in der Praxis lohnt es sich vielleicht nicht.
Austin Malerba
3

Da der Status in Funktionskomponenten nicht eng mit der Komponenteninstanz gekoppelt ist, kann der vorherige Status nicht erreicht werden, useEffectohne ihn zuerst zu speichern, z. B. mit useRef. Dies bedeutet auch, dass die Statusaktualisierung möglicherweise falsch an einer falschen Stelle implementiert wurde, da der vorherige Status darin verfügbar istsetState Statusaktualisierung Updater-Funktion .

Dies ist ein guter Anwendungsfall für useReducer den ein Redux-ähnlicher Speicher bereitgestellt wird und der die Implementierung eines entsprechenden Musters ermöglicht. Statusaktualisierungen werden explizit durchgeführt, sodass nicht herausgefunden werden muss, welche Statuseigenschaft aktualisiert wird. Dies geht bereits aus der versendeten Aktion hervor.

Hier ist ein Beispiel, wie es aussehen könnte:

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
});
...
Estus Flask
quelle
Hallo @estus, danke für diese Idee. Es gibt mir eine andere Denkweise. Ich habe jedoch vergessen zu erwähnen, dass ich für jeden Fall die API aufrufen muss. Haben Sie eine Lösung dafür?
Rwinzhang
Was meinst du? Innengriff Ändern? Bedeutet "API" einen Remote-API-Endpunkt? Können Sie Codesandbox aus der Antwort mit Details aktualisieren?
Estus Flask
Aus dem Update kann ich annehmen, dass Sie sendAmountusw. asynchron abrufen möchten , anstatt sie zu berechnen, oder? Das ändert sich sehr. Es ist möglich, dies zu tun useReduce, kann aber schwierig sein. Wenn Sie sich zuvor mit Redux befasst haben, wissen Sie möglicherweise, dass asynchrone Vorgänge dort nicht einfach sind. github.com/reduxjs/redux-thunk ist eine beliebte Erweiterung für Redux, die asynchrone Aktionen ermöglicht. Hier ist eine Demo, die useReducer mit demselben Muster (useThunkReducer), Codesandbox.io/s/6z4r79ymwr , erweitert . Beachten Sie, dass die versendete Funktion lautet async, dass Sie dort Anforderungen ausführen können (kommentiert).
Estus Flask
1
Das Problem mit der Frage ist, dass Sie nach einer anderen Sache gefragt haben, die nicht Ihr eigentlicher Fall war, und sie dann geändert haben. Jetzt ist es eine ganz andere Frage, während vorhandene Antworten so aussehen, als hätten sie die Frage einfach ignoriert. Dies ist keine empfohlene Vorgehensweise für SO, da es Ihnen nicht hilft, eine gewünschte Antwort zu erhalten. Bitte entfernen Sie keine wichtigen Teile aus der Frage, auf die sich die Antworten bereits beziehen (Berechnungsformeln). Wenn Sie immer noch Probleme haben (nach meinem vorherigen Kommentar (wahrscheinlich auch)), sollten Sie eine neue Frage stellen, die Ihren Fall widerspiegelt, und als vorherigen Versuch einen Link zu dieser Frage
erstellen
Hallo estus, ich denke, diese Codesandbox-Codesandbox.io/s/6z4r79ymwr ist noch nicht aktualisiert. Ich werde die Frage zurück ändern, sorry dafür.
Rwinzhang
3

Die Verwendung von Ref führt zu einer neuen Art von Fehler in der App.

Sehen wir uns diesen Fall mit usePreviousdem zuvor kommentierten an:

  1. prop.minTime: 5 ==> ref.current = 5 | setze ref.current
  2. prop.minTime: 5 ==> ref.current = 5 | neuer Wert ist gleich ref.current
  3. prop.minTime: 8 ==> ref.current = 5 | neuer Wert ist NICHT gleich ref.current
  4. prop.minTime: 5 ==> ref.current = 5 | neuer Wert ist gleich ref.current

Wie wir hier sehen können, aktualisieren refwir das interne nicht, weil wir es verwendenuseEffect

santomegonzalo
quelle
1
React hat dieses Beispiel, aber sie verwenden das state... und nicht das props... Wenn Sie sich für die alten Requisiten interessieren, wird ein Fehler auftreten.
Santomegonzalo
3

Für einen wirklich einfachen Vergleich der Requisiten können Sie mit useEffect leicht überprüfen, ob eine Requisite aktualisiert wurde.

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

}

useEffect führt Ihren Code dann nur aus, wenn sich die Requisite ändert.

Charlie Tupman
quelle
1
Dies funktioniert nur einmal, nach aufeinanderfolgenden propÄnderungen können Sie nicht~ do stuff here ~
Karolis Šarapnickis
2
Scheint, als sollten Sie setHasPropChanged(false)am Ende von anrufen, um ~ do stuff here ~Ihren Status zurückzusetzen. (Aber dies würde in einem zusätzlichen Renderer zurückgesetzt)
Kevin Wang
Vielen Dank für das Feedback, Sie haben beide Recht, aktualisierte Lösung
Charlie Tupman
@ AntonioPavicevac-Ortiz Ich habe die Antwort aktualisiert, um das propHasChanged jetzt als wahr zu rendern, was es dann beim Rendern einmal aufrufen würde. Dies könnte eine bessere Lösung sein, nur um den useEffect herauszureißen und nur das prop zu überprüfen
Charlie Tupman
Ich denke, meine ursprüngliche Verwendung ist verloren gegangen. Wenn Sie auf den Code zurückblicken, können Sie useEffect
Charlie Tupman
2

Die akzeptierte Antwort ist eine alternative Lösung, für die kein benutzerdefinierter Hook erforderlich ist:

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
quelle
1
Gute Lösung, enthält aber Syntaxfehler. useRefnicht userRef. Ich habe vergessen, Strom zu verwendenprevAmount.current
Blake Plumb
0

Hier ist ein benutzerdefinierter Hook, den ich verwende und der meiner Meinung nach intuitiver ist als die Verwendung 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)
}

Sie würden useTransitionwie folgt verwenden.

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

Hoffentlich hilft das.

Aadit M Shah
quelle