¿Cómo comparar oldValues ​​y newValues ​​en React Hooks useEffect?

151

Digamos que tengo 3 entradas: rate, sendAmount y recibenAmount. Puse esas 3 entradas en useEffect diffing params. Las reglas son:

  • Si sendAmount cambió, calculo receiveAmount = sendAmount * rate
  • Si recibo Monto cambiado, calculo sendAmount = receiveAmount / rate
  • Si la tasa cambió, calculo receiveAmount = sendAmount * ratecuándo sendAmount > 0o calculo sendAmount = receiveAmount / ratecuándoreceiveAmount > 0

Aquí está el codesandbox https://codesandbox.io/s/pkl6vn7x6j para demostrar el problema.

¿Hay alguna manera de comparar el oldValuesy me newValuesgusta en componentDidUpdatelugar de hacer 3 controladores para este caso?

Gracias


Aquí está mi solución final con usePrevious https://codesandbox.io/s/30n01w2r06

En este caso, no puedo usar múltiples useEffectporque cada cambio conduce a la misma llamada de red. Es por eso que también uso changeCountpara rastrear el cambio también. Esto changeCounttambién es útil para realizar un seguimiento de los cambios solo locales, por lo que puedo evitar llamadas de red innecesarias debido a los cambios del servidor.

rwinzhang
fuente
¿Cómo se supone que debe ayudar exactamente componentDidUpdate? Aún necesitará escribir estas 3 condiciones.
Estus Flask
Agregué una respuesta con 2 soluciones opcionales. ¿Uno de ellos te funciona?
Ben Carp

Respuestas:

210

Puede escribir un gancho personalizado para proporcionarle accesorios anteriores usando useRef

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

y luego usarlo en 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])
}

Sin embargo, es más claro y probablemente mejor y más claro de leer y comprender si usa dos useEffectpor separado para cada ID de cambio que desea procesarlos por separado

Shubham Khatri
fuente
8
Gracias por la nota sobre el uso de dos useEffectllamadas por separado. ¡No sabía que podía usarlo varias veces!
JasonH
3
Probé el código anterior, pero eslint me advierte que useEffectfaltan dependenciasprevAmount
Littlee
2
Dado que prevAmount tiene el valor del valor anterior de estado / accesorios, no necesita pasarlo como una dependencia para usar Effect y puede deshabilitar esta advertencia para el caso particular. ¿Puedes leer esta publicación para más detalles?
Shubham Khatri
Me encanta esto: useRef siempre viene al rescate.
Fernando Rojo
@ShubhamKhatri: ¿Cuál es la razón de la envoltura useEffect de ref.current = value?
curly_brackets
40

En caso de que alguien esté buscando una versión de uso TypeScript Anterior:

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
fuente
2
Tenga en cuenta que esto no funcionará en un archivo TSX, para que esto funcione en un archivo TSX cambie a const usePrevious = <T extends any>(...hacer que el intérprete vea que <T> no es JSX y es una restricción genérica
apokryfos
1
¿Puedes explicar por qué <T extends {}>no funcionará? Parece estar funcionando para mí, pero estoy tratando de entender la complejidad de usarlo así.
Karthikeyan_kk
.tsxlos archivos contienen jsx, que debe ser idéntico a html. Es posible que el genérico <T>sea ​​una etiqueta de componente no cerrada.
SeedyROM
¿Qué pasa con el tipo de devolución? TengoMissing return type on function.eslint(@typescript-eslint/explicit-function-return-type)
Saba Ahang
@SabaAhang ¡Lo siento! TS utilizará la inferencia de tipos para manejar los tipos de retorno en su mayor parte, pero si ha habilitado el linting, he agregado un tipo de retorno para eliminar las advertencias.
SeedyROM
25

Opción 1 - useComparar gancho

La comparación de un valor actual con un valor anterior es un patrón común, y justifica un enlace personalizado propio que oculta los detalles de la implementación.

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

Manifestación

Opción 2: ejecute useEffect cuando cambie el valor

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

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

Manifestación

Ben Carp
fuente
La segunda opción funcionó para mí. ¿Me puede guiar por qué tengo que escribir useEffect Twice?
Tarun Nagpal
@TarunNagpal, no necesariamente necesita usar useEffect dos veces. Depende de su caso de uso. Imagine que solo queremos registrar qué valor ha cambiado. Si tenemos tanto val1 como val2 en nuestra matriz de dependencias, se ejecutará cada vez que alguno de los valores haya cambiado. Luego, dentro de la función que pasamos, tendremos que descubrir qué val ha cambiado para registrar el mensaje correcto.
Ben Carp
Gracias por la respuesta. Cuando dices "dentro de la función que pasamos, tendremos que averiguar qué valor ha cambiado". Cómo podemos lograrlo. Por favor guía
Tarun Nagpal
6

Acabo de publicar react-delta que resuelve este tipo exacto de escenario. En mi opinión, useEffecttiene demasiadas responsabilidades.

Responsabilidades

  1. Compara todos los valores en su matriz de dependencias usando Object.is
  2. Ejecuta devoluciones de llamada de efecto / limpieza basadas en el resultado del n. ° 1

Rompiendo responsabilidades

react-deltarompe useEffectlas responsabilidades en varios ganchos más pequeños.

Responsabilidad # 1

Responsabilidad # 2

En mi experiencia, este enfoque es más flexible, limpio y conciso que useEffect/ useRefsoluciones.

Austin Malerba
fuente
Justo lo que estoy buscando. ¡Voy a probarlo!
hackerl33t
se ve muy bien, pero ¿no es un poco excesivo?
Hisato
@Hisato, muy bien podría ser excesivo. Es algo así como una API experimental. Y, francamente, no lo he usado mucho en mis equipos porque no es ampliamente conocido o adoptado. En teoría suena un poco agradable, pero en la práctica puede no valer la pena.
Austin Malerba
3

Dado que el estado no está estrechamente acoplado con la instancia de componente en componentes funcionales, no se puede alcanzar el estado anterior useEffectsin guardarlo primero, por ejemplo, con useRef. Esto también significa que la actualización de estado posiblemente se implementó incorrectamente en un lugar incorrecto porque el estado anterior está disponible dentro de la setStatefunción de actualización.

Este es un buen caso de uso para el useReducercual proporciona una tienda similar a Redux y permite implementar el patrón respectivo. Las actualizaciones de estado se realizan explícitamente, por lo que no es necesario averiguar qué propiedad de estado se actualiza; Esto ya está claro en la acción enviada.

Aquí hay un ejemplo de cómo se vería:

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
fuente
Hola @estus, gracias por esta idea. Me da otra forma de pensar. Pero, olvidé mencionar que necesito llamar a API para cada caso. ¿Tienes alguna solución para eso?
rwinzhang el
¿Qué quieres decir? Asa interior ¿Cambiar? ¿'API' significa punto final remoto de API? ¿Puedes actualizar codesandbox desde la respuesta con detalles?
Estus Flask
A partir de la actualización, puedo suponer que desea buscar sendAmount, etc. de forma asincrónica en lugar de calcularlos, ¿verdad? Esto cambia mucho. Es posible hacer esto con, useReducepero puede ser complicado. Si trató con Redux antes, puede saber que las operaciones asíncronas no son sencillas allí. github.com/reduxjs/redux-thunk es una extensión popular para Redux que permite acciones asíncronas. Aquí hay una demostración que aumenta useReducer con el mismo patrón (useThunkReducer), codesandbox.io/s/6z4r79ymwr . Tenga en cuenta que la función despachada es async, puede hacer solicitudes allí (comentado).
Estus Flas el
1
El problema con la pregunta es que usted preguntó sobre algo diferente que no era su caso real y luego lo cambió, por lo que ahora es una pregunta muy diferente, mientras que las respuestas existentes aparecen como si simplemente ignoraran la pregunta. Esta no es una práctica recomendada en SO porque no le ayuda a obtener la respuesta que desea. Por favor, no elimine partes importantes de la pregunta a la que ya se refieren las respuestas (fórmulas de cálculo). Si aún tiene problemas (después de mi comentario anterior (probablemente lo haga), considere hacer una nueva pregunta que refleje su caso y enlace a este como su intento anterior.
Estus Flask
Hola estus, creo que este codesandbox codesandbox.io/s/6z4r79ymwr no está actualizado todavía. Cambiaré de nuevo la pregunta, lo siento.
rwinzhang
3

El uso de Ref introducirá un nuevo tipo de error en la aplicación.

Veamos este caso usando lo usePreviousque alguien comentó antes:

  1. prop.minTime: 5 ==> ref.current = 5 | set ref.current
  2. prop.minTime: 5 ==> ref.current = 5 | el nuevo valor es igual a la corriente de referencia
  3. prop.minTime: 8 ==> ref.current = 5 | el nuevo valor NO es igual a la corriente de referencia
  4. prop.minTime: 5 ==> ref.current = 5 | el nuevo valor es igual a la corriente de referencia

Como podemos ver aquí, no estamos actualizando el interno refporque estamos usandouseEffect

santomegonzalo
fuente
1
React tiene este ejemplo, pero están usando el state... y no el props... cuando te preocupas por los accesorios viejos, entonces se producirá un error.
santomegonzalo
3

Para una comparación de accesorios realmente simple, puede usar useEffect para verificar fácilmente si un accesorio se ha actualizado.

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

}

useEffect solo ejecutará su código si el accesorio cambia.

Charlie Tupman
fuente
1
Esto solo funciona una vez, después de propcambios consecutivos no podrás~ do stuff here ~
Karolis Šarapnickis
2
Parece que debería llamar setHasPropChanged(false)al final de ~ do stuff here ~"restablecer" su estado. (Pero esto sería restablecer en un reRender adicional)
Kevin Wang
Gracias por los comentarios, ambos tienen la solución correcta y actualizada
Charlie Tupman
@ AntonioPavicevac-Ortiz He actualizado la respuesta para que ahora el propHasChanged sea verdadero, que luego lo llamaría una vez en el renderizado, podría ser una mejor solución solo para eliminar el useEffect y simplemente verificar el prop
Charlie Tupman
Creo que mi uso original de esto se ha perdido. Mirando hacia atrás en el código, puede usar useEffect
Charlie Tupman
2

Saliendo de la respuesta aceptada, una solución alternativa que no requiere un enlace 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
fuente
1
Buena solución, pero contiene errores de sintaxis. useRefno userRef. Olvidé usar currentprevAmount.current
Blake Plumb
0

Aquí hay un gancho personalizado que uso que creo que es más intuitivo 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)
}

Lo usarías de la useTransitionsiguiente manera.

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 ayude.

Aadit M Shah
fuente