Como comparar oldValues ​​e newValues ​​em React Hooks useEffect?

149

Digamos que eu tenho 3 entradas: rate, sendAmount e receiveAmount. Coloquei essas 3 entradas nos parâmetros diferentes useEffect. As regras são:

  • Se sendAmount for alterado, eu calculo receiveAmount = sendAmount * rate
  • Se receiveAmount mudou, eu calculo sendAmount = receiveAmount / rate
  • Se a taxa for alterada, calculo receiveAmount = sendAmount * ratequando sendAmount > 0ou calculo sendAmount = receiveAmount / ratequandoreceiveAmount > 0

Aqui está a codesandbox https://codesandbox.io/s/pkl6vn7x6j para demonstrar o problema.

Existe uma maneira de comparar o oldValuese newValuescomo em componentDidUpdatevez de fazer 3 manipuladores para este caso?

obrigado


Aqui está minha solução final com usePrevious https://codesandbox.io/s/30n01w2r06

Nesse caso, não posso usar vários useEffectporque cada alteração está levando à mesma chamada de rede. É por isso que eu também uso changeCountpara acompanhar a mudança. Isso changeCounttambém é útil para rastrear alterações somente do local, para que eu possa impedir chamadas de rede desnecessárias devido a alterações do servidor.

rwinzhang
fonte
Como exatamente o componentDidUpdate deve ajudar? Você ainda precisará escrever essas três condições.
Estus Flask
Eu adicionei uma resposta com 2 soluções opcionais. Um deles funciona para você?
Ben Carp

Respostas:

207

Você pode escrever um gancho personalizado para fornecer adereços anteriores usando useRef

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

e depois use-o 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])
}

No entanto, é mais claro e provavelmente melhor e mais claro de ler e entender se você usar dois useEffectseparadamente para cada ID de mudança, deseja processá-los separadamente

Shubham Khatri
fonte
8
Obrigado pela observação sobre o uso de duas useEffectchamadas separadamente. Não sabia que você poderia usá-lo várias vezes!
JasonH
3
Eu tentei o código acima, mas eslint está me alertando que useEffectdependências que estão faltandoprevAmount
Littlee
2
Como prevAmount mantém um valor para o valor anterior de state / props, não é necessário passá-lo como uma dependência para useEffect e é possível desativar esse aviso para o caso específico. Você pode ler este post para obter mais detalhes?
Shubham Khatri
Amo isso - useRef sempre vem em socorro.
Fernando Rojo
@ShubhamKhatri: Qual o motivo do empacotamento useEffect de ref.current = value?
curly_brackets 30/01
40

Caso alguém esteja procurando uma versão de uso do 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
fonte
2
Note que isso não vai funcionar em um arquivo de TSX, para chegar a este trabalho em uma alteração de arquivo TSX para const usePrevious = <T extends any>(...fazer o intérprete veja pensar que <T> não é JSX e é uma restrição genérica
apokryfos
1
Você pode explicar por <T extends {}>que não vai funcionar. Parece estar funcionando para mim, mas estou tentando entender a complexidade de usá-lo dessa maneira.
precisa
.tsxOs arquivos contêm jsx, que deve ser idêntico ao html. É possível que o genérico <T>seja uma tag de componente não fechada.
SeedyROM 12/12/19
E o tipo de retorno? Eu entendoMissing return type on function.eslint(@typescript-eslint/explicit-function-return-type)
Saba Ahang
@SabaAhang Sorry! O TS usará a inferência de tipo para lidar com os tipos de retorno, na maioria das vezes, mas se você tiver ativado o linting, adicionei um tipo de retorno para se livrar dos avisos.
SeedyROM 9/03
25

Opção 1 - useCompare hook

Comparar um valor atual a um valor anterior é um padrão comum e justifica um gancho personalizado próprio que oculta os detalhes da implementação.

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

Opção 2 - execute useEffect quando o valor for alterado

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

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

Demo

Ben Carp
fonte
Segunda opção funcionou para mim. Você pode me orientar por que tenho que escrever useEffect Twice?
Tarun Nagpal 27/03
@TarunNagpal, você não precisa necessariamente usar o useEffect duas vezes. Depende do seu caso de uso. Imagine que apenas queremos registrar qual val mudou. Se tivermos val1 e val2 em nossa matriz de dependências, ele será executado sempre que qualquer um dos valores for alterado. Então, dentro da função que passamos, teremos que descobrir qual val mudou para registrar a mensagem correta.
Ben Carp
Obrigado pela resposta. Quando você diz "dentro da função que passamos, teremos que descobrir qual val mudou". Como podemos alcançá-lo. Guia
Tarun Nagpal
6

Acabei de publicar react-delta, que resolve esse tipo exato de cenário. Na minha opinião, useEffecttem muitas responsabilidades.

Responsabilidades

  1. Ele compara todos os valores em sua matriz de dependência usando Object.is
  2. Ele executa retornos de chamada de efeito / limpeza com base no resultado do nº 1

Quebrando responsabilidades

react-deltadivide useEffectas responsabilidades em vários ganchos menores.

Responsabilidade # 1

Responsabilidade # 2

Na minha experiência, essa abordagem é mais flexível, limpa e concisa do que useEffect/ useRefsolutions.

Austin Malerba
fonte
Apenas o que estou procurando. Vou experimentar!
hackerl33t 24/04
parece realmente bom, mas não é um pouco exagerado?
Hisato
@Hisato muito bem pode ser um exagero. É algo como uma API experimental. E, francamente, não usei muito em minhas equipes porque não é amplamente conhecido ou adotado. Em teoria, parece meio legal, mas na prática pode não valer a pena.
Austin Malerba
3

Como o estado não está fortemente associado à instância do componente nos componentes funcionais, o estado anterior não pode ser alcançado useEffectsem salvá-lo primeiro, por exemplo, com useRef. Isso também significa que a atualização do estado possivelmente foi implementada incorretamente no lugar errado, porque o estado anterior está disponível na setStatefunção atualizador.

Este é um bom caso de uso para o useReducerqual fornece armazenamento semelhante ao Redux e permite implementar o respectivo padrão. As atualizações de estado são executadas explicitamente, portanto, não há necessidade de descobrir qual propriedade de estado é atualizada; isso já está claro na ação despachada.

Aqui está um exemplo de como pode ser:

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
fonte
Olá @estus, obrigado por esta ideia. Isso me dá outra maneira de pensar. Mas esqueci de mencionar que preciso chamar a API para cada caso. Você tem alguma solução para isso?
rwinzhang
O que você quer dizer? Por dentro handleChange? 'API' significa ponto de extremidade da API remota? Você pode atualizar codesandbox a partir da resposta com detalhes?
amigos estão dizendo sobre estus
A partir da atualização, posso assumir que você deseja buscar sendAmount, etc, de forma assíncrona, em vez de calculá-las, certo? Isso muda muito. É possível fazer isso com, useReducemas pode ser complicado. Se você lidou com o Redux antes de saber que as operações assíncronas não são simples lá. O github.com/reduxjs/redux-thunk é uma extensão popular do Redux que permite ações assíncronas. Aqui está uma demonstração que aumenta useReducer com o mesmo padrão (useThunkReducer), codesandbox.io/s/6z4r79ymwr . Observe que a função despachada é async, você pode fazer solicitações lá (comentadas).
Estus Flask
1
O problema com a pergunta é que você perguntou sobre uma coisa diferente que não era o seu caso real e a alterou, então agora é uma pergunta muito diferente enquanto as respostas existentes aparecem como se elas simplesmente ignorassem a pergunta. Esta não é uma prática recomendada no SO, pois não ajuda a obter a resposta desejada. Por favor, não remova partes importantes da pergunta a que as respostas já se referem (fórmulas de cálculo). Se você ainda tiver problemas (depois do meu comentário anterior (provavelmente)), considere fazer uma nova pergunta que reflita seu caso e faça um link para este como sua tentativa anterior.
Estus Flask
Oi estus, acho que este codesandbox codesandbox.io/s/6z4r79ymwr ainda não foi atualizado. Voltarei a pergunta, desculpe por isso.
rwinzhang
3

O uso de Ref introduzirá um novo tipo de bug no aplicativo.

Vamos ver este caso usando usePreviousalguém que comentou antes:

  1. prop.minTime: 5 ==> ref.current = 5 | definir ref.current
  2. prop.minTime: 5 ==> ref.current = 5 | novo valor é igual a ref.current
  3. prop.minTime: 8 ==> ref.current = 5 | novo valor NÃO é igual a ref.current
  4. prop.minTime: 5 ==> ref.current = 5 | novo valor é igual a ref.current

Como podemos ver aqui, não estamos atualizando o interno refporque estamos usandouseEffect

santomegonzalo
fonte
1
O React possui este exemplo, mas eles estão usando o state... e não o props... quando você se preocupa com os adereços antigos, o erro ocorre.
Santomegonzalo 28/11/19
3

Para uma comparação realmente simples de objetos propostos, você pode usar o useEffect para verificar facilmente se um objeto foi atualizado.

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

}

useEffect executará seu código somente se o suporte for alterado.

Charlie Tupman
fonte
1
Isso funciona apenas uma vez, após propalterações consecutivas, você não poderá mais~ do stuff here ~
Karolis Šarapnickis 10/10/1919
2
Parece que você deve ligar setHasPropChanged(false)no final de ~ do stuff here ~"redefinir" seu estado. (Mas isso iria repor em uma renderizados adicional)
Kevin Wang
Obrigado pelo feedback, você está certo, solução atualizada
Charlie Tupman 02/06
@ AntonioPavicevac-Ortiz Atualizei a resposta para agora renderizar o propHasChanged como true, que o chamaria uma vez no render, pode ser uma solução melhor apenas para eliminar o useEffect e apenas verificar o prop
Charlie Tupman em
Eu acho que meu uso original disso foi perdido. Olhando para o código, você pode simplesmente usar useEffect
Charlie Tupman
2

Saindo da resposta aceita, uma solução alternativa que não requer um gancho personalizado:

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
fonte
1
Boa solução, mas contém erros de sintaxe. useRefnão userRef. Esqueci de usar a correnteprevAmount.current
Blake Plumb
0

Aqui está um gancho personalizado que eu acredito que é mais intuitivo do que usar 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)
}

Você usaria da useTransitionseguinte maneira.

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

Espero que ajude.

Aadit M Shah
fonte