Bucle infinito en uso

105

He estado jugando con el nuevo sistema de enlace en React 16.7-alpha y me quedo atascado en un bucle infinito en useEffect cuando el estado que estoy manejando es un objeto o matriz.

Primero, uso useState y lo inicio con un objeto vacío como este:

const [obj, setObj] = useState({});

Luego, en useEffect, uso setObj para volver a establecerlo en un objeto vacío. Como segundo argumento, paso [obj], con la esperanza de que no se actualice si el contenido del objeto no ha cambiado. Pero sigue actualizándose. Supongo que porque no importa el contenido, estos siempre son objetos diferentes que hacen que React piense que sigue cambiando.

useEffect(() => {
  setIngredients({});
}, [ingredients]);

Lo mismo ocurre con las matrices, pero como primitiva no se atasca en un bucle, como se esperaba.

Usando estos nuevos ganchos, ¿cómo debo manejar los objetos y la matriz al verificar si el contenido ha cambiado o no?

Tobias Haugen
fuente
Tobias, ¿qué caso de uso requiere cambiar el valor de los ingredientes, una vez que su valor ha cambiado?
Ben Carp
@Tobias, deberías leer mi respuesta. Estoy seguro de que la aceptará como la respuesta correcta.
HalfWebDev

Respuestas:

123

Pasar una matriz vacía como segundo argumento para useEffect hace que solo se ejecute en mount y unmount, deteniendo así cualquier bucle infinito.

useEffect(() => {
  setIngredients({});
}, []);

Esto me fue aclarado en la publicación del blog sobre React hooks en https://www.robinwieruch.de/react-hooks/

Tobias Haugen
fuente
1
En realidad, es una matriz vacía, no un objeto vacío que debe pasar.
GifCo
16
Esto no resuelve el problema, debe pasar las dependencias utilizadas por el gancho
helado
1
Tenga en cuenta que al desmontar el efecto ejecutará una función de limpieza si ha especificado una. El efecto real no se ejecuta al desmontar. reactjs.org/docs/hooks-effect.html#example-using-hooks-1
tony
2
Usar una matriz vacía cuando setIngrediets es una dependencia es un antipatrón, como dijo Dan Abramov. No trate useEffetct como un método componentDidMount ()
p7adams
1
Dado que las personas anteriores no proporcionaron un enlace o recurso sobre cómo resolver correctamente este problema, sugiero a los visitantes que se acercan a los patos que vean esto, ya que solucionó mi problema de bucle infinito: stackoverflow.com/questions/56657907/…
C. Rib
72

Tuvo el mismo problema. No sé por qué no mencionan esto en los documentos. Solo quiero agregar un poco a la respuesta de Tobias Haugen.

Para ejecutar en todos los componentes / reenvío padre , debe usar:

  useEffect(() => {

    // don't know where it can be used :/
  })

Para ejecutar algo solo una vez después del montaje del componente (se renderizará una vez), debe usar:

  useEffect(() => {

    // do anything only one time if you pass empty array []
    // keep in mind, that component will be rendered one time (with default values) before we get here
  }, [] )

Para ejecutar algo una vez en el montaje de componentes y en el cambio de datos / datos2 :

  const [data, setData] = useState(false)
  const [data2, setData2] = useState('default value for first render')
  useEffect(() => {

// if you pass some variable, than component will rerender after component mount one time and second time if this(in my case data or data2) is changed
// if your data is object and you want to trigger this when property of object changed, clone object like this let clone = JSON.parse(JSON.stringify(data)), change it clone.prop = 2 and setData(clone).
// if you do like this 'data.prop=2' without cloning useEffect will not be triggered, because link to data object in momory doesn't changed, even if object changed (as i understand this)
  }, [data, data2] )

Cómo lo uso la mayor parte del tiempo:

export default function Book({id}) { 
  const [book, bookSet] = useState(false) 

  useEffect(() => {
    loadBookFromServer()
  }, [id]) // every time id changed, new book will be loaded

  // Remeber that it's not always safe to omit functions from the list of dependencies. Explained in comments.
  async function loadBookFromServer() {
    let response = await fetch('api/book/' + id)
    response  = await response.json() 
    bookSet(response)
  }

  if (!book) return false //first render, when useEffect did't triggered yet we will return false

  return <div>{JSON.stringify(book)}</div>  
}
ZiiMakc
fuente
2
De acuerdo con las preguntas frecuentes de React, no es seguro omitir funciones de la lista de dependencias .
egdavid
@endavid depende de cuál es el accesorio que está utilizando
ZiiMakc
1
por supuesto, si no utiliza ningún valor del ámbito del componente, es seguro omitirlo. Pero desde un punto de vue puramente de diseño / arquitectura, esta no es una buena práctica, ya que requiere que mueva toda su función dentro del efecto si necesita usar accesorios y podría terminar con un método useEffect que usará un cantidad escandalosa de líneas de código.
egdavid
16

También me encontré con el mismo problema una vez y lo solucioné asegurándome de pasar valores primitivos en el segundo argumento [].

Si pasa un objeto, React almacenará solo la referencia al objeto y ejecutará el efecto cuando cambie la referencia, que suele ser cada vez (aunque no sé cómo).

La solución es pasar los valores en el objeto. Puedes probar,

const obj = { keyA: 'a', keyB: 'b' }

useEffect(() => {
  // do something
}, [Object.values(obj)]);

o

const obj = { keyA: 'a', keyB: 'b' }

useEffect(() => {
  // do something
}, [obj.keyA, obj.keyB]);
Dinesh Pandiyan
fuente
3
Otro enfoque es crear tales valores con useMemo, de esa manera la referencia se mantiene y los valores dependencias se evalúan como el mismo
helado
@helado puede usar useMemo () para valores o useCallback () para funciones
Juanma Menendez
11

Como se dice en la documentación ( https://reactjs.org/docs/hooks-effect.html ), el useEffectgancho está destinado a ser utilizado cuando desee que se ejecute algún código después de cada renderizado . De los documentos:

¿UseEffect se ejecuta después de cada renderizado? ¡Si!

Si desea personalizar esto, puede seguir las instrucciones que aparecen más adelante en la misma página ( https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects ). Básicamente, el useEffectmétodo acepta un segundo argumento , que React examinará para determinar si el efecto debe activarse nuevamente o no.

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes

Puede pasar cualquier objeto como segundo argumento. Si este objeto permanece sin cambios, su efecto solo se activará después de la primera montura . Si el objeto cambia, el efecto se activará nuevamente.

Rocío García Luque
fuente
11

Si está creando un gancho personalizado , a veces puede causar un bucle infinito con el valor predeterminado de la siguiente manera

function useMyBadHook(values = {}) {
    useEffect(()=> { 
           /* This runs every render, if values is undefined */
        },
        [values] 
    )
}

La solución es usar el mismo objeto en lugar de crear uno nuevo en cada llamada de función:

const defaultValues = {};
function useMyBadHook(values = defaultValues) {
    useEffect(()=> { 
           /* This runs on first call and when values change */
        },
        [values] 
    )
}

Si se encuentra con esto en el código de su componente, el bucle puede arreglarse si usa defaultProps en lugar de los valores predeterminados de ES6

function MyComponent({values}) {
  useEffect(()=> { 
       /* do stuff*/
    },[values] 
  )
  return null; /* stuff */
}

MyComponent.defaultProps = {
  values = {}
}
Jkarttunen
fuente
¡Gracias! Esto no era obvio para mí y era exactamente el problema con el que me estaba encontrando.
atascado el
1
¡Salvaste mi día! Gracias hermano.
Han Van Pham
6

No estoy seguro de si esto funcionará para usted, pero podría intentar agregar .length como este:

useEffect(() => {
        // fetch from server and set as obj
}, [obj.length]);

En mi caso (¡estaba obteniendo una matriz!), Obtuvo datos en el montaje, luego nuevamente solo en el cambio y no entró en un bucle.

Luka M
fuente
1
¿Qué pasa si se reemplaza un elemento en la matriz? En ese caso, la longitud de la matriz sería la misma y el efecto no se ejecutaría.
Aamir Khan
5

Si incluye una matriz vacía al final de useEffect :

useEffect(()=>{
        setText(text);
},[])

Funcionaría una vez.

Si incluye también el parámetro en la matriz:

useEffect(()=>{
            setText(text);
},[text])

Se ejecutará siempre que cambie el parámetro de texto.

besartm
fuente
¿Por qué se ejecutaría solo una vez si colocamos una matriz vacía al final de un gancho?
Karen
Una matriz vacía al final de un useEffect es una implementación intencionada por parte de los desarrolladores para detener bucles infinitos en situaciones en las que, por ejemplo, es posible que necesite establecer un estado dentro de un useEffect. De lo contrario, esto conduciría a useEffect -> actualización de estado -> useEffect -> bucle infinito.
to240
4

Tu bucle infinito se debe a la circularidad

useEffect(() => {
  setIngredients({});
}, [ingredients]);

setIngredients({});cambiará el valor de ingredients(devolverá una nueva referencia cada vez), que se ejecutará setIngredients({}). Para resolver esto, puede usar cualquiera de los enfoques:

  1. Pase un segundo argumento diferente para useEffect
const timeToChangeIngrediants = .....
useEffect(() => {
  setIngredients({});
}, [timeToChangeIngrediants ]);

setIngrediantsse ejecutará cuando timeToChangeIngrediantshaya cambiado.

  1. No estoy seguro de qué caso de uso justifica el cambio de ingredientes una vez que se ha cambiado. Pero si es el caso, pasa Object.values(ingrediants)como segundo argumento a useEffect.
useEffect(() => {
  setIngredients({});
}, Object.values(ingrediants));
Ben Carp
fuente
2

Si usa esta optimización, asegúrese de que la matriz incluya todos los valores del alcance del componente (como los accesorios y el estado) que cambian con el tiempo y que son usados ​​por el efecto.

Creo que están tratando de expresar la posibilidad de que uno pueda estar usando datos obsoletos y ser conscientes de esto. No importa el tipo de valores que enviemos arraypara el segundo argumento siempre que sepamos que si alguno de esos valores cambia, se ejecutará el efecto. Si lo estamos utilizando ingredientscomo parte del cálculo dentro del efecto, deberíamos incluirlo en el array.

const [ingredients, setIngredients] = useState({});

// This will be an infinite loop, because by shallow comparison ingredients !== {} 
useEffect(() => {
  setIngredients({});
}, [ingredients]);

// If we need to update ingredients then we need to manually confirm 
// that it is actually different by deep comparison.

useEffect(() => {
  if (is(<similar_object>, ingredients) {
    return;
  }
  setIngredients(<similar_object>);
}, [ingredients]);

locomotora
fuente
1

La mejor manera es comparar el valor anterior con el valor actual usando usePrevious () y _.isEqual () de Lodash . Importe isEqual y useRef . Compare su valor anterior con el valor actual dentro de useEffect () . Si son iguales, no actualice nada más. usePrevious (value) es un gancho personalizado que crea una referencia con useRef () .

A continuación se muestra un fragmento de mi código. Estaba enfrentando un problema de bucle infinito con la actualización de datos usando firebase hook

import React, { useState, useEffect, useRef } from 'react'
import 'firebase/database'
import { Redirect } from 'react-router-dom'
import { isEqual } from 'lodash'
import {
  useUserStatistics
} from '../../hooks/firebase-hooks'

export function TMDPage({ match, history, location }) {
  const usePrevious = value => {
    const ref = useRef()
    useEffect(() => {
      ref.current = value
    })
    return ref.current
  }
  const userId = match.params ? match.params.id : ''
  const teamId = location.state ? location.state.teamId : ''
  const [userStatistics] = useUserStatistics(userId, teamId)
  const previousUserStatistics = usePrevious(userStatistics)

  useEffect(() => {
      if (
        !isEqual(userStatistics, previousUserStatistics)
      ) {
        
        doSomething()
      }
     
  })

HS Progr
fuente
1
Supongo que la gente rechazó esto porque sugirió usar una biblioteca de terceros. Sin embargo, la idea subyacente es buena.
jperl
1

En caso de que necesite comparar el objeto y cuando se actualice, aquí hay un deepComparegancho para comparar. La respuesta aceptada seguramente no aborda eso. Tener una []matriz es adecuado si necesita que el efecto se ejecute solo una vez cuando esté montado.

Además, otras respuestas votadas solo abordan una verificación de tipos primitivos haciendo obj.valueo algo similar para llegar primero al nivel donde no está anidado. Este puede no ser el mejor caso para objetos profundamente anidados.

Así que aquí hay uno que funcionará en todos los casos.

import { DependencyList } from "react";

const useDeepCompare = (
    value: DependencyList | undefined
): DependencyList | undefined => {
    const ref = useRef<DependencyList | undefined>();
    if (!isEqual(ref.current, value)) {
        ref.current = value;
    }
    return ref.current;
};

Puedes usar lo mismo en useEffectgancho

React.useEffect(() => {
        setState(state);
    }, useDeepCompare([state]));
HalfWebDev
fuente
0

También puede desestructurar el objeto en la matriz de dependencia, lo que significa que el estado solo se actualizará cuando se actualicen ciertas partes del objeto.

Por el bien de este ejemplo, digamos que los ingredientes contienen zanahorias, podríamos pasar eso a la dependencia, y solo si las zanahorias cambiaran, el estado se actualizaría.

Luego, podría llevar esto más allá y solo actualizar la cantidad de zanahorias en ciertos puntos, controlando así cuándo se actualizaría el estado y evitando un bucle infinito.

useEffect(() => {
  setIngredients({});
}, [ingredients.carrots]);

Un ejemplo de cuándo podría usarse algo como esto es cuando un usuario inicia sesión en un sitio web. Cuando inician sesión, podemos desestructurar el objeto de usuario para extraer su función de cookies y permisos, y actualizar el estado de la aplicación en consecuencia.

to240
fuente
0

mi caso fue especial al encontrar un bucle infinito, el senario era así:

Tenía un objeto, digamos objX que proviene de accesorios y lo estaba desestructurando en accesorios como:

const { something: { somePropery } } = ObjX

y utilicé el someProperycomo una dependencia a mi useEffectgusto:


useEffect(() => {
  // ...
}, [somePropery])

y me causó un bucle infinito, traté de manejar esto pasando el conjunto somethingcomo una dependencia y funcionó correctamente.

a_m_dev
fuente