El método set useState no refleja un cambio inmediatamente

Resuelto Pranjal asked hace 5 años • 19 respuestas

Estoy tratando de aprender ganchos y el useStatemétodo me ha confundido. Estoy asignando un valor inicial a un estado en forma de matriz. El método set useStateno me funciona, ni con ni sin la sintaxis extendida.

Creé una API en otra PC a la que llamo y obtengo los datos que quiero configurar en el estado.

Aquí está mi código:

<div id="root"></div>

<script type="text/babel" defer>
// import React, { useState, useEffect } from "react";
// import ReactDOM from "react-dom";
const { useState, useEffect } = React; // web-browser variant

const StateSelector = () => {
  const initialValue = [
    {
      category: "",
      photo: "",
      description: "",
      id: 0,
      name: "",
      rating: 0
    }
  ];

  const [movies, setMovies] = useState(initialValue);

  useEffect(() => {
    (async function() {
      try {
        // const response = await fetch("http://192.168.1.164:5000/movies/display");
        // const json = await response.json();
        // const result = json.data.result;
        const result = [
          {
            category: "cat1",
            description: "desc1",
            id: "1546514491119",
            name: "randomname2",
            photo: null,
            rating: "3"
          },
          {
            category: "cat2",
            description: "desc1",
            id: "1546837819818",
            name: "randomname1",
            rating: "5"
          }
        ];
        console.log("result =", result);
        setMovies(result);
        console.log("movies =", movies);
      } catch (e) {
        console.error(e);
      }
    })();
  }, []);

  return <p>hello</p>;
};

const rootElement = document.getElementById("root");
ReactDOM.render(<StateSelector />, rootElement);
</script>

<script src="https://unpkg.com/@babel/standalone@7/babel.min.js"></script>
<script src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
Expandir fragmento

Ni setMovies(result)ni setMovies(...result)funciona.

Espero que la resultvariable se inserte en la moviesmatriz.

Pranjal avatar Jan 07 '19 13:01 Pranjal
Aceptado

Al igual que .setState()en los componentes de clase creados mediante la extensión React.Componento React.PureComponent, la actualización del estado mediante el actualizador proporcionado por useStateel enlace también es asincrónica y no se reflejará de inmediato.

Además, el problema principal aquí no es solo la naturaleza asincrónica, sino el hecho de que las funciones utilizan los valores de estado en función de sus cierres actuales, y las actualizaciones de estado se reflejarán en la próxima representación en la que los cierres existentes no se ven afectados, pero sí los nuevos. unos se crean . Ahora, en el estado actual, los valores dentro de los ganchos se obtienen mediante cierres existentes, y cuando ocurre una nueva representación, los cierres se actualizan en función de si la función se recrea nuevamente o no.

Incluso si agrega setTimeoutuna función, aunque el tiempo de espera se ejecutará después de un tiempo en el cual se habría vuelto a renderizar, seguirá setTimeoutusando el valor de su cierre anterior y no el actualizado.

setMovies(result);
console.log(movies) // movies here will not be updated

Si desea realizar una acción en la actualización del estado, necesita usar el useEffectgancho, muy parecido a usar componentDidUpdatecomponentes en la clase, ya que el configurador devuelto useStateno tiene un patrón de devolución de llamada.

useEffect(() => {
    // action on update of movies
}, [movies]);

En lo que respecta a la sintaxis para actualizar el estado, setMovies(result)reemplazará el moviesvalor anterior en el estado con los disponibles en la solicitud asíncrona.

Sin embargo, si desea fusionar la respuesta con los valores previamente existentes, debe usar la sintaxis de devolución de llamada de actualización de estado junto con el uso correcto de la sintaxis extendida como

setMovies(prevMovies => ([...prevMovies, ...result]));
Shubham Khatri avatar Jan 07 '2019 06:01 Shubham Khatri

Detalles adicionales a la respuesta anterior :

Si bien React setState es asincrónico (tanto clases como ganchos) y es tentador utilizar ese hecho para explicar el comportamiento observado, no es la razón por la que sucede.

TLDR: el motivo es un alcance de cierre en torno a un valor inmutable const.


Soluciones:

  • lea el valor en la función de renderizado (no dentro de funciones anidadas):

      useEffect(() => { setMovies(result) }, [])
      console.log(movies)
    
  • agregue la variable a las dependencias (y use la regla eslint reaccionar-hooks/exhaustive-deps ):

      useEffect(() => { setMovies(result) }, [])
      useEffect(() => { console.log(movies) }, [movies])
    
  • utilizar una variable temporal:

      useEffect(() => {
        const newMovies = result
        console.log(newMovies)
        setMovies(newMovies)
      }, [])
    
  • use una referencia mutable (si no necesitamos un estado y solo queremos recordar el valor, actualizar una referencia no activa la repetición):

      const moviesRef = useRef(initialValue)
      useEffect(() => {
        moviesRef.current = result
        console.log(moviesRef.current)
      }, [])
    

Explicación por qué sucede:

Si async fuera la única razón, sería posible await setState().

Sin embargo, se supone que ambos propsy no cambian durante 1 render .state

Trátelo this.statecomo si fuera inmutable.

Con los ganchos, esta suposición se mejora mediante el uso de valores constantes con la constpalabra clave:

const [state, setState] = useState('initial')

El valor puede ser diferente entre 2 renderizados, pero permanece constante dentro del renderizado mismo y dentro de cualquier cierre (funciones que duran más incluso después de finalizar el renderizado, por ejemplo useEffect, controladores de eventos, dentro de cualquier Promise o setTimeout).

Considere la siguiente implementación falsa, pero síncrona , similar a React:

// sync implementation:

let internalState
let renderAgain

const setState = (updateFn) => {
  internalState = updateFn(internalState)
  renderAgain()
}

const useState = (defaultState) => {
  if (!internalState) {
    internalState = defaultState
  }
  return [internalState, setState]
}

const render = (component, node) => {
  const {html, handleClick} = component()
  node.innerHTML = html
  renderAgain = () => render(component, node)
  return handleClick
}

// test:

const MyComponent = () => {
  const [x, setX] = useState(1)
  console.log('in render:', x) // ✅
  
  const handleClick = () => {
    setX(current => current + 1)
    console.log('in handler/effect/Promise/setTimeout:', x) // ❌ NOT updated
  }
  
  return {
    html: `<button>${x}</button>`,
    handleClick
  }
}

const triggerClick = render(MyComponent, document.getElementById('root'))
triggerClick()
triggerClick()
triggerClick()
<div id="root"></div>
Expandir fragmento

Aprillion avatar Nov 15 '2019 13:11 Aprillion

Sé que ya hay muy buenas respuestas. Pero quiero dar otra idea de cómo resolver el mismo problema y acceder al último estado de la 'película', usando mi módulo reaccionar-useStateRef que tiene más de 11,000 descargas semanales.

Como comprenderá, al usar el estado de React, puede representar la página cada vez que cambia el estado. Pero al usar React ref, siempre puede obtener los valores más recientes.

Entonces, el módulo react-useStateRefle permite usar estados y referencias juntos. Es compatible con versiones anteriores React.useState, por lo que puedes simplemente reemplazar la importdeclaración

const { useEffect } = React
import { useState } from 'react-usestateref'

  const [movies, setMovies] = useState(initialValue);

  useEffect(() => {
    (async function() {
      try {

        const result = [
          {
            id: "1546514491119",
          },
        ];
        console.log("result =", result);
        setMovies(result);
        console.log("movies =", movies.current); // will give you the latest results
      } catch (e) {
        console.error(e);
      }
    })();
  }, []);

Más información:

  • reaccionar-usestsateref
Aminadav Glickshtein avatar Dec 24 '2020 09:12 Aminadav Glickshtein