Jak porównać stare i nowe wartości w React hookach useEffect?

149

Powiedzmy, że mam 3 wejścia: rate, sendAmount i acceptAmount. Umieściłem te 3 wejścia na różnicowych parametrach useEffect. Zasady są następujące:

  • Jeśli wartość sendAmount uległa zmianie, obliczam receiveAmount = sendAmount * rate
  • Jeśli wartość otrzymanej kwoty uległa zmianie, obliczam sendAmount = receiveAmount / rate
  • Jeśli kurs się zmienił, obliczam receiveAmount = sendAmount * ratekiedy sendAmount > 0lub obliczam sendAmount = receiveAmount / ratekiedyreceiveAmount > 0

Oto kodyandbox https://codesandbox.io/s/pkl6vn7x6j, aby zademonstrować problem.

Czy istnieje sposób na porównanie oldValuesi newValuespolubienie componentDidUpdatezamiast tworzenia 3 programów obsługi dla tego przypadku?

Dzięki


Oto moje ostateczne rozwiązanie z usePrevious https://codesandbox.io/s/30n01w2r06

W tym przypadku nie mogę użyć wielu, useEffectponieważ każda zmiana prowadzi do tego samego połączenia sieciowego. Dlatego też używam changeCountdo śledzenia zmian. Jest to changeCountrównież pomocne w śledzeniu zmian tylko z lokalnego, więc mogę zapobiec niepotrzebnym połączeniom sieciowym z powodu zmian z serwera.

rwinzhang
źródło
Jak dokładnie ma pomóc składnik componentDidUpdate? Nadal będziesz musiał napisać te 3 warunki.
Estus Flask
Dodałem odpowiedź z 2 opcjonalnymi rozwiązaniami. Czy któryś z nich działa dla Ciebie?
Ben Carp

Odpowiedzi:

208

Możesz napisać niestandardowy punkt zaczepienia, który zapewni ci poprzednie właściwości za pomocą useRef

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

a następnie użyj go w 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])
}

Jednak jest to jaśniejsze i prawdopodobnie lepsze i bardziej przejrzyste do odczytania i zrozumienia, jeśli używasz dwóch useEffectosobno dla każdego identyfikatora zmiany, który chcesz przetwarzać osobno

Shubham Khatri
źródło
8
Dziękuję za uwagę dotyczącą korzystania z dwóch useEffectpołączeń oddzielnie. Nie wiedziałem, że możesz go używać wiele razy!
JasonH
3
Wypróbowałem powyższy kod, ale eslint ostrzega mnie, że useEffectbrakuje zależnościprevAmount
Littlee
2
Ponieważ prevAmount przechowuje wartość dla poprzedniej wartości state / props, nie musisz przekazywać jej jako zależności, aby useEffect i możesz wyłączyć to ostrzeżenie dla konkretnego przypadku. Możesz przeczytać ten post, aby uzyskać więcej informacji?
Shubham Khatri
Uwielbiam to - useRef zawsze przychodzi na ratunek.
Fernando Rojo
@ShubhamKhatri: Jaki jest powód zawijania useEffect dla ref.current = value?
curly_brackets
40

O ile ktoś szuka wersji do wykorzystania w języku 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
źródło
2
Zauważ, że to nie zadziała w pliku TSX, aby to zadziałało w pliku TSX, zmień go, const usePrevious = <T extends any>(...aby interpreter zobaczył, że <T> to nie JSX i jest to ogólne ograniczenie
apokryfos
1
Czy możesz wyjaśnić, dlaczego <T extends {}>nie zadziała. Wydaje się, że to działa dla mnie, ale próbuję zrozumieć złożoność korzystania z niego w ten sposób.
Karthikeyan_kk
.tsxpliki zawierają jsx, co ma być identyczne z html. Możliwe, że rodzaj ogólny <T>jest niezamkniętym znacznikiem komponentu.
SeedyROM,
A co z typem zwrotu? DostajęMissing return type on function.eslint(@typescript-eslint/explicit-function-return-type)
Saba Ahang
@SabaAhang Przepraszamy! TS użyje wnioskowania o typie do obsługi typów zwracanych w większości przypadków, ale jeśli masz włączone lintowanie, dodałem typ zwracany, aby pozbyć się ostrzeżeń.
SeedyROM
25

Opcja 1 - useCompare hook

Porównanie bieżącej wartości z poprzednią wartością jest powszechnym wzorcem i uzasadnia własny niestandardowy punkt zaczepienia, który ukrywa szczegóły implementacji.

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

Próbny

Opcja 2 - uruchom useEffect po zmianie wartości

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

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

Próbny

Ben Carp
źródło
Druga opcja zadziałała dla mnie. Czy możesz mi pomóc, dlaczego muszę pisać dwukrotnie useEffect?
Tarun Nagpal
@TarunNagpal, niekoniecznie musisz dwukrotnie użyć useEffect. To zależy od twojego przypadku użycia. Wyobraź sobie, że chcemy tylko zarejestrować, która wartość uległa zmianie. Jeśli w naszej tablicy zależności mamy zarówno wart1, jak i wart2, będzie ona uruchamiana za każdym razem, gdy zmieni się którakolwiek z wartości. Następnie w funkcji, którą przekazujemy, będziemy musieli dowiedzieć się, która wartość została zmieniona, aby zarejestrować poprawną wiadomość.
Ben Carp
Dziękuję za odpowiedź. Kiedy powiesz „wewnątrz funkcji, którą przekazujemy, będziemy musieli dowiedzieć się, która wartość ma zmianę”. Jak możemy to osiągnąć. Proszę o przewodnik
Tarun Nagpal
6

Właśnie opublikowałem odpowiedź -delta, która rozwiązuje dokładnie tego rodzaju scenariusz. Moim zdaniem useEffectma zbyt wiele obowiązków.

Obowiązki

  1. Porównuje wszystkie wartości w swojej tablicy zależności przy użyciu Object.is
  2. Uruchamia wywołania zwrotne efektów / czyszczenia w oparciu o wynik # 1

Zerwanie odpowiedzialności

react-deltadzieli useEffectobowiązki na kilka mniejszych haczyków.

Odpowiedzialność nr 1

Odpowiedzialność nr 2

Z mojego doświadczenia wynika, że ​​to podejście jest bardziej elastyczne, przejrzyste i zwięzłe niż useEffect/ useRefrozwiązania.

Austin Malerba
źródło
Właśnie tego szukam. Spróbuję tego!
hackerl33t
wygląda naprawdę dobrze, ale czy to nie przesada?
Hisato
@Hisato, to może być przesada. To coś w rodzaju eksperymentalnego interfejsu API. I szczerze mówiąc, nie używałem go zbyt często w swoich zespołach, ponieważ nie jest powszechnie znany ani przyjęty. W teorii brzmi to ładnie, ale w praktyce może nie być tego warte.
Austin Malerba
3

Ponieważ stan nie jest ściśle powiązany z instancją komponentu w komponentach funkcjonalnych, nie można osiągnąć poprzedniego stanu useEffectbez wcześniejszego zapisania go, na przykład za pomocą useRef. Oznacza to również, że aktualizacja stanu została prawdopodobnie nieprawidłowo zaimplementowana w niewłaściwym miejscu, ponieważ poprzedni stan jest dostępny w setStatefunkcji aktualizatora.

Jest to dobry przypadek użycia, useReducerktóry zapewnia sklep podobny do Redux i pozwala na implementację odpowiedniego wzorca. Aktualizacje stanu są wykonywane jawnie, więc nie ma potrzeby sprawdzania, która właściwość stanu jest aktualizowana; widać to już po wysłanej akcji.

Oto przykład, jak to może wyglądać:

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
źródło
Cześć @estus, dzięki za ten pomysł. Daje mi inny sposób myślenia. Ale zapomniałem wspomnieć, że muszę wywołać API dla każdego przypadku. Czy masz na to jakieś rozwiązanie?
rwinzhang
Co masz na myśli? Uchwyt wewnętrzny Zmienić? Czy „API” oznacza zdalny punkt końcowy API? Czy możesz zaktualizować kody w odpowiedzi ze szczegółami?
Estus Flask
Z aktualizacji mogę założyć, że chcesz pobierać sendAmountitp. Asynchronicznie zamiast je obliczać, prawda? To się bardzo zmienia. Można to zrobić, useReduceale może to być trudne. Jeśli zajmowałeś się Reduxem, możesz wiedzieć, że operacje asynchroniczne nie są tam proste. github.com/reduxjs/redux-thunk to popularne rozszerzenie dla Redux, które pozwala na akcje asynchroniczne. Oto demo, które rozszerza useReducer o ten sam wzorzec (useThunkReducer) , codeandbox.io/s/6z4r79ymwr . Zauważ, że wysłana funkcja jest taka async, że możesz tam wykonywać żądania (skomentowane).
Estus Flask
1
Problem z tym pytaniem polega na tym, że zapytałeś o inną rzecz, która nie była twoim prawdziwym przypadkiem, a następnie ją zmieniłeś, więc teraz jest to zupełnie inne pytanie, podczas gdy istniejące odpowiedzi wyglądają tak, jakby po prostu zignorowały pytanie. Nie jest to zalecana praktyka w SO, ponieważ nie pomaga w uzyskaniu oczekiwanej odpowiedzi. Proszę nie usuwać ważnych części z pytania, do którego już odnoszą się odpowiedzi (wzory obliczeniowe). Jeśli nadal masz problemy (po moim poprzednim komentarzu (prawdopodobnie tak), rozważ zadanie nowego pytania, które odzwierciedla Twój przypadek i umieść link do tego jako poprzedniej próby.
Estus Flask
Cześć estus, myślę, że te kody i kody skrzynekandbox.io/s/6z4r79ymwr nie są jeszcze aktualizowane. Zmienię pytanie, przepraszam za to.
rwinzhang
3

Użycie Ref wprowadzi do aplikacji nowy rodzaj błędu.

Zobaczmy ten przypadek, korzystając z usePreviouswcześniejszego komentarza:

  1. prop.minTime: 5 ==> ref.current = 5 | ustaw ref. prąd
  2. prop.minTime: 5 ==> ref.current = 5 | nowa wartość jest równa ref.current
  3. prop.minTime: 8 ==> ref.current = 5 | nowa wartość NIE jest równa ref.current
  4. prop.minTime: 5 ==> ref.current = 5 | nowa wartość jest równa ref.current

Jak widać tutaj, nie aktualizujemy wewnętrznego, refponieważ używamyuseEffect

santomegonzalo
źródło
1
React ma ten przykład, ale używa state... a nie props... kiedy zależy ci na starych rekwizytach, wtedy wystąpi błąd.
santomegonzalo
3

Dla naprawdę prostego porównania rekwizytów możesz użyć useEffect, aby łatwo sprawdzić, czy właściwość została zaktualizowana.

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

}

useEffect uruchomi wtedy Twój kod tylko wtedy, gdy właściwość ulegnie zmianie.

Charlie Tupman
źródło
1
Działa to tylko raz, po kolejnych propzmianach nie będziesz w stanie~ do stuff here ~
Karolis Šarapnickis
2
Wygląda na to, że powinieneś zadzwonić setHasPropChanged(false)pod koniec, ~ do stuff here ~aby „zresetować” swój stan. (Ale to by się zresetowało w dodatkowym wydaniu)
Kevin Wang
Dzięki za opinię, oboje macie rację, zaktualizowane rozwiązanie
Charlie Tupman
@ AntonioPavicevac-Ortiz Zaktualizowałem odpowiedź, aby teraz renderować propHasChanged jako true, co następnie nazwałoby to po renderowaniu, może być lepszym rozwiązaniem po prostu wyrwać useEffect i po prostu sprawdzić rekwizyt
Charlie Tupman
Myślę, że moje pierwotne użycie tego zostało utracone. Patrząc wstecz na kod, możesz po prostu użyć useEffect
Charlie Tupman
2

Wychodząc od zaakceptowanej odpowiedzi, alternatywne rozwiązanie, które nie wymaga niestandardowego zaczepu:

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
źródło
1
Dobre rozwiązanie, ale zawiera błędy składniowe. useRefnie userRef. Zapomniałem użyć prąduprevAmount.current
Blake Plumb
0

Oto niestandardowy hak, którego używam, a który moim zdaniem jest bardziej intuicyjny niż używanie 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)
}

Używałbyś useTransitionw następujący sposób.

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

Mam nadzieję, że to pomoże.

Aadit M Shah
źródło