useEffect: evita el bucle infinito al actualizar el estado

9

Me gustaría que el usuario pueda ordenar una lista de elementos pendientes. Cuando los usuarios seleccionan un elemento de un menú desplegable, establecerán el sortKeyque creará una nueva versión setSortedTodosy, a su vez, activará useEffecty llamará setSortedTodos.

El siguiente ejemplo funciona exactamente como quiero, sin embargo, eslint me está pidiendo que agregue todosal useEffectconjunto de dependencias, y si lo hago, causa un bucle infinito (como era de esperar).

const [todos, setTodos] = useState([]);
const [sortKey, setSortKey] = useState('title');

const setSortedTodos = useCallback((data) => {
  const cloned = data.slice(0);

  const sorted = cloned.sort((a, b) => {
    const v1 = a[sortKey].toLowerCase();
    const v2 = b[sortKey].toLowerCase();

    if (v1 < v2) {
      return -1;
    }

    if (v1 > v2) {
      return 1;
    }

    return 0;
  });

  setTodos(sorted);
}, [sortKey]);

useEffect(() => {
    setSortedTodos(todos);
}, [setSortedTodos]);

Ejemplo en vivo:

Estoy pensando que tiene que haber una mejor manera de hacer esto que mantenga feliz a Eslint.

DanV
fuente
1
Solo una nota al margen: la sortdevolución de llamada puede ser justa: lo return a[sortKey].toLowerCase().localeCompare(b[sortKey].toLowerCase());que también tiene la ventaja de hacer una comparación local si el entorno tiene información local razonable. Si lo desea, también puede lanzarle desestructuración: pastebin.com/7X4M1XTH
TJ Crowder
¿Qué error está eslintlanzando?
Luze
¿Podría actualizar la pregunta para proporcionar un ejemplo reproducible mínimo reproducible del problema utilizando fragmentos de pila (el [<>]botón de la barra de herramientas)? Los fragmentos de pila admiten React, incluido JSX; He aquí cómo hacer uno . De esa manera, las personas pueden verificar que sus soluciones propuestas no tengan el problema del bucle infinito ...
TJ Crowder
Ese es un enfoque realmente interesante y un problema realmente interesante. Como usted dice, puede entender por qué ESLint cree que necesita agregar todosa la matriz de dependencias useEffect, y puede ver por qué no debería hacerlo. :-)
TJ Crowder
Agregué el ejemplo en vivo para ti. Realmente quiero ver esto respondido.
TJ Crowder

Respuestas:

8

Yo diría que esto significa que hacerlo de esta manera no es lo ideal. La función depende de hecho todos. Si setTodosse llama a otro lugar, la función de devolución de llamada debe ser recalculada, de lo contrario, funciona con datos obsoletos.

¿Por qué almacena la matriz ordenada en estado de todos modos? Puede usar useMemopara ordenar los valores cuando cambia la clave o la matriz:

const sortedTodos = useMemo(() => {
  return Array.from(todos).sort((a, b) => {
    const v1 = a[sortKey].toLowerCase();
    const v2 = b[sortKey].toLowerCase();

    if (v1 < v2) {
      return -1;
    }

    if (v1 > v2) {
      return 1;
    }

    return 0;
  });
}, [sortKey, todos]);

Luego referencia en sortedTodostodas partes.

Ejemplo en vivo:

No es necesario almacenar los valores ordenados en estado, ya que siempre puede derivar / calcular la matriz ordenada a partir de la matriz "base" y la clave de ordenación. Yo diría que también hace que su código sea más fácil de entender, ya que es menos complejo.

Felix Kling
fuente
Oh buen uso de useMemo. Solo una pregunta secundaria, ¿por qué no usar .localCompareen el género?
Tikkes
2
De hecho, eso sería mejor. Acabo de copiar el código del OP y no presté atención a eso (no es realmente relevante para el problema).
Felix Kling
Solución realmente simple, fácil de entender.
TJ Crowder
Ah sí, use Memo! Olvidé eso :)
DanV
3

La razón del bucle infinito es porque todos no coinciden con la referencia anterior y el efecto se volverá a ejecutar.

¿Por qué usar un efecto para una acción de clic de todos modos? Puede ejecutarlo en una función como esta:

const [todos, setTodos] = useState([]);

function sortTodos(e) {
    const sortKey = e.target.value;
    const clonedTodos = [...todos];
    const sorted = clonedTodos.sort((a, b) => {
        return a[sortKey.toLowerCase()].localeCompare(b[sortKey.toLowerCase()]);
    });

    setTodos(sorted);
}

y en tu menú desplegable haz un onChange.

    <select onChange="sortTodos"> ......

Nota sobre la dependencia por cierto, ¡ESLint tiene razón! Sus Todos, en el caso descrito anteriormente, son una dependencia y deberían estar en la lista. El enfoque en la selección de un artículo es incorrecto, y de ahí su problema.

Tikkes
fuente
2
"sort devolverá una nueva instancia de una matriz" No es el método de ordenación incorporado. Ordena la matriz en el lugar. data.slice(0)crea la copia
Felix Kling
Eso no es cierto cuando haces un setStateya que no editará el objeto existente y por lo tanto lo clonará internamente. Redacción incorrecta en mi respuesta, cierto. Yo editaré esto.
Tikkes
setStateno clona datos. ¿Por qué piensas eso?
Felix Kling
1
@Tikkes - No, setStateno clona nada. Felix y el OP son correctos, debe copiar la matriz antes de ordenarla.
TJ Crowder
Ok si, lo siento. Parece que necesito leer un poco más sobre lo interno. De hecho, tiene que copiar y no cambiar lo existente state.
Tikkes
0

Lo que debe hacer aquí es usar una forma funcional de setState:

  const [todos, setTodos] = useState(exampleToDos);
    const [sortKey, setSortKey] = useState('title');

    const setSortedTodos = useCallback((data) => {

      setTodos(currTodos => {
        return currTodos.sort((a, b) => {
          const v1 = a[sortKey].toLowerCase();
          const v2 = b[sortKey].toLowerCase();

          if (v1 < v2) {
            return -1;
          }

          if (v1 > v2) {
            return 1;
          }

          return 0;
        });
      })

    }, [sortKey]);

    useEffect(() => {
        setSortedTodos(todos);
    }, [setSortedTodos, todos]);

Códigos de trabajo y caja

Incluso si está copiando el estado para no mutar el original, todavía no se garantiza que obtendrá su último valor, debido a que el estado de configuración es asíncrono. Además, la mayoría de los métodos devolverán una copia superficial, por lo que puede terminar mutando el estado original de todos modos.

El uso de funcional setStategarantiza que obtenga el último valor del estado y no mute el valor del estado original.

Claridad
fuente
"y no mutar el valor de estado original". No creo que React pase una copia a la devolución de llamada. .sortcambia la matriz en el lugar, por lo que aún tendrá que copiarla usted mismo.
Felix Kling
Hmm, buen punto, en realidad no puedo encontrar ninguna confirmación de que pase la copia del estado, aunque recuerdo haber leído algo así antes.
Claridad
Lo encontré aquí: reactjs.org/docs/… . 'podemos usar la forma de actualización funcional de setState. Nos permite especificar cómo debe cambiar el estado sin hacer referencia al estado actual '
Claridad
No leo eso como obtener una copia. Simplemente significa que no tiene que proporcionar el estado actual como dependencia "externa".
Felix Kling el