useState 设置方法没有立即反映变化
我正在尝试学习钩子,但
useState
方法让我感到困惑。我正在以数组的形式为状态分配初始值。
useState
中的 set 方法对我来说不起作用,无论是否使用扩展语法。
我在另一台电脑上创建了一个 API,我正在调用它并获取我想要设置到状态中的数据。
这是我的代码:
<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>
setMovies(result)
和
setMovies(...result)
都不起作用。
我希望
result
变量被推送到
movies
数组中。
与通过扩展
React.Component
或
React.PureComponent
创建的类组件中的
.setState()
非常相似
,使用
useState
钩子提供的更新程序进行的状态更新也是异步的,不会立即反映出来。
此外,这里的主要问题不仅仅是异步性,而是函数根据其当前闭包使用状态值的事实,状态更新将反映在下一次重新渲染中,现有闭包不受影响,但会创建新的闭包 。现在,在当前状态下,钩子内的值由现有的闭包获取,当重新渲染发生时,闭包会根据函数是否再次重新创建进行更新。
即使你在函数中添加了
setTimeout
,虽然超时会在重新渲染发生的一段时间后运行,但
setTimeout
仍将使用其前一个闭包中的值,而不是更新后的闭包。
setMovies(result);
console.log(movies) // movies here will not be updated
如果要在状态更新时执行操作,则需要使用
useEffect
钩子,就像在类组件中使用
componentDidUpdate
一样,因为
useState
返回的 setter 没有回调模式
useEffect(() => {
// action on update of movies
}, [movies]);
就更新状态的语法而言,
setMovies(result)
将取代状态中的前一个
movies
值与异步请求中可用的值。
但是,如果您想将响应与先前存在的值合并,则必须使用状态更新的回调语法以及正确使用扩展语法,如
setMovies(prevMovies => ([...prevMovies, ...result]));
有关 上一个答案 的更多详细信息:
虽然 React 的
setState
是
异步的(类和钩子都是),并且很容易用这个事实来解释观察到的行为,但这并不是
为什么
发生这种情况的原因。
TLDR:原因是围绕不可变
const
值的
闭包
作用域。
解决方案:
-
在 render 函数中读取值(不在嵌套函数内)函数):
useEffect(() => { setMovies(result) }, []) console.log(movies)
-
将变量添加到依赖项中(并使用 react-hooks/exhaustive-deps eslint 规则):
useEffect(() => { setMovies(result) }, []) useEffect(() => { console.log(movies) }, [movies])
-
使用临时变量:
useEffect(() => { const newMovies = result console.log(newMovies) setMovies(newMovies) }, [])
-
使用可变引用(如果我们不需要状态而只想记住值 - 更新引用不会触发重新渲染):
const moviesRef = useRef(initialValue) useEffect(() => { moviesRef.current = result console.log(moviesRef.current) }, [])
解释为什么会发生这种情况:
如果异步是唯一的原因,可以
await setState()
。
但是,
props
和
state
都被
假定在 1 次渲染期间保持不变
。
Treat
this.state
as if it were immutable.
使用钩子,可以通过将
常量值
与
const
关键字结合使用来增强此假设:
const [state, setState] = useState('initial')
两次渲染之间的值可能不同,但在渲染本身内部以及任何
闭包
(渲染完成后仍存活更长时间的函数,例如
useEffect
,事件处理程序,在任何 Promise 或 setTimeout 内)。
考虑以下虚假的但 同步 的类似 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>
我知道已经有非常好的答案了。但我想给出另一个想法来解决同一问题,并使用我的模块 react-useStateRef 访问最新的“电影”状态,该模块每周下载量超过 11,000 次。
正如您所理解的,通过使用 React state,您可以在每次状态更改时呈现页面。但通过使用 React ref,您始终可以获得最新值。
因此,模块
react-useStateRef
允许您一起使用 state 和 ref。它向后兼容
React.useState
,因此您只需替换
import
语句即可。
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);
}
})();
}, []);