El método set useState no refleja un cambio inmediatamente
Estoy tratando de aprender ganchos y el useState
método me ha confundido. Estoy asignando un valor inicial a un estado en forma de matriz. El método set useState
no 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>
Ni setMovies(result)
ni setMovies(...result)
funciona.
Espero que la result
variable se inserte en la movies
matriz.
Al igual que .setState()
en los componentes de clase creados mediante la extensión React.Component
o React.PureComponent
, la actualización del estado mediante el actualizador proporcionado por useState
el 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 setTimeout
una función, aunque el tiempo de espera se ejecutará después de un tiempo en el cual se habría vuelto a renderizar, seguirá setTimeout
usando 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 useEffect
gancho, muy parecido a usar componentDidUpdate
componentes en la clase, ya que el configurador devuelto useState
no 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 movies
valor 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]));
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 props
y no cambian durante 1 render .state
Trátelo
this.state
como si fuera inmutable.
Con los ganchos, esta suposición se mejora mediante el uso de valores constantes con la const
palabra 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>
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-useStateRef
le permite usar estados y referencias juntos. Es compatible con versiones anteriores React.useState
, por lo que puedes simplemente reemplazar la import
declaració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