useState 设置方法不会立即反映更改
Posted
技术标签:
【中文标题】useState 设置方法不会立即反映更改【英文标题】:useState set method not reflecting change immediately 【发布时间】:2021-08-26 04:36:48 【问题描述】:我正在尝试学习钩子,useState
方法让我感到困惑。我正在以数组的形式为状态分配初始值。 useState
中的 set 方法对我不起作用,无论有没有扩展运算符。
我在另一台 PC 上创建了一个 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)
都不起作用。
我希望将结果变量推入电影数组。
【问题讨论】:
您能否看到将console.log("movies =", movies);
移动到useEffect
挂钩之外的更改?
这在a meta question中用作示例。
【参考方案1】:
类似于扩展React.Component
或React.PureComponent
创建的Class组件中的setState,使用useState
钩子提供的updater更新状态也是异步的,不会立即反映。
此外,这里的主要问题不仅仅是异步性质,而是函数基于当前闭包使用状态值这一事实,并且状态更新将反映在现有闭包的下一次重新渲染中不受影响,但会创建新的。现在在当前状态下,钩子中的值是通过现有的闭包获得的,当重新渲染发生时,闭包会根据函数是否再次重新创建而更新。
即使你添加了 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]));
【讨论】:
嗨,在表单提交处理程序中调用 useState 怎么样?我正在验证一个复杂的表单,我在 submitHandler useState 钩子中调用,不幸的是,更改不是立即的!useEffect
可能不是最好的解决方案,因为它不支持异步调用。因此,如果我们想对 movies
状态更改进行一些异步验证,我们无法控制它。
请注意,虽然建议非常好,但对原因的解释可以改进 - 与 the updater provided by useState hook
是否异步这一事实无关,不像 this.state
如果this.setState
是同步的,则发生突变,即使useState
提供了同步函数,const movies
周围的闭包也将保持不变-请参阅我的答案中的示例
setMovies(prevMovies => ([...prevMovies, ...result]));
为我工作
它记录了错误的结果,因为您正在记录 stale closure 而不是因为 setter 是异步的。如果异步是问题所在,那么您可以在超时后记录,但您可以设置一个小时的超时并仍然记录错误的结果,因为异步不是导致问题的原因。【参考方案2】:
previous answer 的其他详细信息:
虽然 React 的 setState
是异步的(类和钩子),并且很想用这个事实来解释观察到的行为,但这不是 原因它发生了。
TLDR:原因是 closure 范围围绕不可变的 const
值。
解决方案:
读取渲染函数中的值(不在嵌套函数中):
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
都是 assumed to be unchanging during 1 render。
将
this.state
视为不可变的。
有了钩子,这个假设通过使用带有const
关键字的常量值得到增强:
const [state, setState] = useState('initial')
2 次渲染之间的值可能不同,但在渲染本身和任何 closures 内保持不变(即使在渲染完成后寿命更长的函数,例如 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>
【讨论】:
优秀的答案。国际海事组织,如果您花足够的时间阅读和吸收它,这个答案将为您在未来节省最多的心碎。 @AlJoslin 乍一看,这似乎是一个单独的问题,即使它可能是由闭包范围引起的。如果您有具体问题,请使用代码示例和所有内容创建一个新的 *** 问题... 实际上,我刚刚完成了使用 useReducer 的重写,遵循@kentcdobs 文章(参考下文),这确实给了我一个可靠的结果,没有受到这些关闭问题的影响。 (参考:kentcdodds.com/blog/how-to-use-react-context-effectively) @ACV 解决方案 2 适用于原始问题。如果您需要解决不同的问题,YMMW,但我仍然 100% 确定引用的代码按文档说明工作,并且问题出在其他地方。 这应该是“接受”答案@Pranjal【参考方案3】:// replace
return <p>hello</p>;
// with
return <p>JSON.stringify(movies)</p>;
现在您应该看到,您的代码确实确实工作。不起作用的是console.log(movies)
。这是因为movies
指向旧状态。如果您将console.log(movies)
移到useEffect
之外,在返回的正上方,您将看到更新后的电影对象。
【讨论】:
不确定为什么这个答案被大量否决,它告诉如何通过将它放在 useState 函数之外来获得“预期的”console.log 值。简单而甜蜜,如果有人想知道为什么会这样,可以参考上面的详细说明【参考方案4】:我刚刚完成了 useReducer 的重写,遵循了@kentcdobs 文章(参考下文),它确实给了我一个可靠的结果,并且没有受到这些关闭问题的影响。
见:https://kentcdodds.com/blog/how-to-use-react-context-effectively
我将他的可读样板压缩到我喜欢的 DRYness 级别——阅读他的沙盒实现将向您展示它的实际工作原理。
import React from 'react'
// ref: https://kentcdodds.com/blog/how-to-use-react-context-effectively
const ApplicationDispatch = React.createContext()
const ApplicationContext = React.createContext()
function stateReducer(state, action)
if (state.hasOwnProperty(action.type))
return ...state, [action.type]: state[action.type] = action.newValue ;
throw new Error(`Unhandled action type: $action.type`);
const initialState =
keyCode: '',
testCode: '',
testMode: false,
phoneNumber: '',
resultCode: null,
mobileInfo: '',
configName: '',
appConfig: ,
;
function DispatchProvider( children )
const [state, dispatch] = React.useReducer(stateReducer, initialState);
return (
<ApplicationDispatch.Provider value=dispatch>
<ApplicationContext.Provider value=state>
children
</ApplicationContext.Provider>
</ApplicationDispatch.Provider>
)
function useDispatchable(stateName)
const context = React.useContext(ApplicationContext);
const dispatch = React.useContext(ApplicationDispatch);
return [context[stateName], newValue => dispatch( type: stateName, newValue )];
function useKeyCode() return useDispatchable('keyCode');
function useTestCode() return useDispatchable('testCode');
function useTestMode() return useDispatchable('testMode');
function usePhoneNumber() return useDispatchable('phoneNumber');
function useResultCode() return useDispatchable('resultCode');
function useMobileInfo() return useDispatchable('mobileInfo');
function useConfigName() return useDispatchable('configName');
function useAppConfig() return useDispatchable('appConfig');
export
DispatchProvider,
useKeyCode,
useTestCode,
useTestMode,
usePhoneNumber,
useResultCode,
useMobileInfo,
useConfigName,
useAppConfig,
类似这样的用法:
import useHistory from "react-router-dom";
// https://react-bootstrap.github.io/components/alerts
import Container, Row from 'react-bootstrap';
import useAppConfig, useKeyCode, usePhoneNumber from '../../ApplicationDispatchProvider';
import ControlSet from '../../components/control-set';
import keypadClass from '../../utils/style-utils';
import MaskedEntry from '../../components/masked-entry';
import Messaging from '../../components/messaging';
import SimpleKeypad, HandleKeyPress, ALT_ID from '../../components/simple-keypad';
export const AltIdPage = () =>
const history = useHistory();
const [keyCode, setKeyCode] = useKeyCode();
const [phoneNumber, setPhoneNumber] = usePhoneNumber();
const [appConfig, setAppConfig] = useAppConfig();
const keyPressed = btn =>
const maxLen = appConfig.phoneNumberEntry.entryLen;
const newValue = HandleKeyPress(btn, phoneNumber).slice(0, maxLen);
setPhoneNumber(newValue);
const doSubmit = () =>
history.push('s');
const disableBtns = phoneNumber.length < appConfig.phoneNumberEntry.entryLen;
return (
<Container fluid className="text-center">
<Row>
<Messaging ... msgColors: appConfig.pageColors, msgLines: appConfig.entryMsgs.altIdMsgs />
</Row>
<Row>
<MaskedEntry ... ...appConfig.phoneNumberEntry, entryColors: appConfig.pageColors, entryLine: phoneNumber />
</Row>
<Row>
<SimpleKeypad ... keyboardName: ALT_ID, themeName: appConfig.keyTheme, keyPressed, styleClass: keypadClass />
</Row>
<Row>
<ControlSet ... btnColors: appConfig.buttonColors, disabled: disableBtns, btns: [ text: 'Submit', click: doSubmit ] />
</Row>
</Container>
);
;
AltIdPage.propTypes = ;
现在我的所有页面上的所有内容都可以顺利进行
【讨论】:
我不认为这个答案在 OP 的上下文中特别有用。这个答案甚至没有使用useState()
,这是 OP 调查的核心。
顺利的解决方案,但不能回答正在发生的事情【参考方案5】:
React useEffect 有自己的状态/生命周期,它与状态的突变有关,在效果销毁之前不会更新状态。
只需在参数状态中传递单个参数或将其保留为黑色数组,它将完美地工作。
React.useEffect(() =>
console.log("effect");
(async () =>
try
let result = await fetch("/query/countries");
const res = await result.json();
let result1 = await fetch("/query/projects");
const res1 = await result1.json();
let result11 = await fetch("/query/regions");
const res11 = await result11.json();
setData(
countries: res,
projects: res1,
regions: res11
);
catch
)(data)
, [setData])
# or use this
useEffect(() =>
(async () =>
try
await Promise.all([
fetch("/query/countries").then((response) => response.json()),
fetch("/query/projects").then((response) => response.json()),
fetch("/query/regions").then((response) => response.json())
]).then(([country, project, region]) =>
// console.log(country, project, region);
setData(
countries: country,
projects: project,
regions: region
);
)
catch
console.log("data fetch error")
)()
, [setData]);
或者,您可以尝试 React.useRef() 以立即更改反应钩子。
const movies = React.useRef(null);
useEffect(() =>
movies.current='values';
console.log(movies.current)
, [])
【讨论】:
最后一个代码示例既不需要 async 也不需要 await,因为您使用 Promise API。这只需要在第一个 伙计,你刚刚在午夜救了我!非常感谢@muhammad【参考方案6】:我知道已经有很好的答案了。但我想给出另一个想法,如何解决同样的问题,并使用我的模块 react-useStateRef 访问最新的“电影”状态。
正如您所理解的,通过使用 React 状态,您可以在每次状态更改时呈现页面。但是通过使用 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);
)();
, []);
更多信息:
react-usestsateref【讨论】:
【参考方案7】:有很多很好的答案可以说明如何修复您的代码,但是有一个 NPM 包可以让您通过更改 import
来修复它。它叫做react-useStateRef。
在你的情况下:
import useState from 'react-usestateref'
const [movies, setMovies,moviesRef] = useState(initialValue);
....
useEffect(() =>
setMovies(...)
console.log(moviesRef.current) // It will have the last value
)
如您所见,使用此库可以让您访问最新状态。
【讨论】:
它不起作用我安装并导入了 useStateRef。但这个问题没有改变。 const [pageCountOwnAlbum, setpageCountOwnAlbum, refOwnAlbum] = useStateRef(1) // 分页自己的相册数据获取 console.log("Value---", refOwnAlbum.current); 给予和以前一样 我有点困惑。你已经用相同的包推荐回答了这个问题already,而这次没有透露你的隶属关系!?【参考方案8】:我觉得这很好。而不是将状态(方法 1)定义为示例,
const initialValue = 1;
const [state,setState] = useState(initialValue)
试试这个方法(方法二),
const [state = initialValue,setState] = useState()
这在不使用 useEffect 的情况下解决了重新渲染问题,因为我们不关心这种情况下的内部关闭方法。
P.S.:如果您担心在任何用例中使用旧状态,则需要使用带有 useEffect 的 useState,因为它需要具有该状态,因此在这种情况下应使用方法 1。
【讨论】:
【参考方案9】:有一种特殊的语法可以以更舒适的方式处理 Promise,称为“async/await”。它非常容易理解和使用。
setState 的这种情况可以有多种解决方案。我发现的最简单舒适的解决方案之一如下:
在函数式组件中,将 useEffect() 设为 async/await。即,在上面的例子中,useEffect() 已经作为一个异步函数。现在做 setMovies(results) as await 像:
await setMovies(results);
这肯定会解决立即更改的问题。关键字 await 让 javascript 等待,直到该承诺完成并返回其结果。
您也不必像问题中所写的那样设置初始值。
你只能声明可变电影,如
const [movies, setMovies] = useState([]);
有关详细信息,请参阅 Async/await。
【讨论】:
setMovies
不返回承诺,因此 async/await
不会真正解决任何问题。它可能看起来有效,但实际上是一个简单的竞争条件。【参考方案10】:
使用我的库中的自定义挂钩,您可以等待状态值更新:
useAsyncWatcher(...values):watcherFn(peekPrevValue: boolean)=>Promise
- 是一个围绕 useEffect 的承诺包装器,它可以等待更新并返回一个新值,如果可选的 peekPrevValue
参数设置为 true,则可能返回前一个值。
(Live Demo)
import React, useState, useEffect, useCallback from "react";
import useAsyncWatcher from "use-async-effect2";
function TestComponent(props)
const [counter, setCounter] = useState(0);
const [text, setText] = useState("");
const textWatcher = useAsyncWatcher(text);
useEffect(() =>
setText(`Counter: $counter`);
, [counter]);
const inc = useCallback(() =>
(async () =>
await new Promise((resolve) => setTimeout(resolve, 1000));
setCounter((counter) => counter + 1);
const updatedText = await textWatcher();
console.log(updatedText);
)();
, []);
return (
<div className="component">
<div className="caption">useAsyncEffect demo</div>
<div>counter</div>
<button onClick=inc>Inc counter</button>
</div>
);
export default TestComponent;
useAsyncDeepState
是一个深度状态实现(类似于 this.setState (patchObject)),它的 setter 可以返回一个与内部效果同步的 Promise。如果不带参数调用 setter,它不会更改状态值,而只是订阅状态更新。在这种情况下,您可以从组件内部的任何位置获取状态值,因为函数闭包不再是障碍。
(Live Demo)
import React, useCallback, useEffect from "react";
import useAsyncDeepState from "use-async-effect2";
function TestComponent(props)
const [state, setState] = useAsyncDeepState(
counter: 0,
computedCounter: 0
);
useEffect(() =>
setState(( counter ) => (
computedCounter: counter * 2
));
, [state.counter]);
const inc = useCallback(() =>
(async () =>
await new Promise((resolve) => setTimeout(resolve, 1000));
await setState(( counter ) => ( counter: counter + 1 ));
console.log("computedCounter=", state.computedCounter);
)();
);
return (
<div className="component">
<div className="caption">useAsyncDeepState demo</div>
<div>state.counter : state.counter</div>
<div>state.computedCounter : state.computedCounter</div>
<button onClick=() => inc()>Inc counter</button>
</div>
);
【讨论】:
【参考方案11】:我在setState
钩子中发现了一个技巧。您不得使用旧变量。您必须创建新变量并将其传递给钩子。例如:
const [users, setUsers] = useState(['Ayşe', 'Fatma'])
useEffect(() =>
setUsers((oldUsers) =>
oldUsers.push(<div>Emir</div>)
oldUsers.push(<div>Buğra</div>)
oldUsers.push(<div>Emre</div>)
return oldUsers
)
, [])
return (
<Fragment>
users
</Fragment>
)
您只会看到Ayşe
和Fatma
用户。因为您正在返回(或传递)oldUsers
变量。此变量与旧状态的引用具有相同的引用。您必须返回 NEWLY CREATED 变量。 如果你传递相同的引用变量,那么 Reactjs 不会更新状态。
const [users, setUsers] = useState(['Ayşe', 'Fatma'])
useEffect(() =>
setUsers((oldUsers) =>
const newUsers = [] // Create new array. This is so important.
// you must push every old item to our newly created array
oldUsers.map((user, index) =>
newUsers.push(user)
)
// NOTE: map() function is synchronous
newUsers.push(<div>Emir</div>)
newUsers.push(<div>Buğra</div>)
newUsers.push(<div>Emre</div>)
return newUsers
)
, [])
return (
<Fragment>
users
</Fragment>
)
【讨论】:
这是一个不相关的问题,而在 OP 的问题中,它最终会更新状态,只是在以后的渲染周期中。 Updating a state array comes with its own issues, which are already addressed on Stack Overflow.【参考方案12】:var [state,setState]=useState(defaultValue)
useEffect(()=>
var updatedState
setState(currentState=> // Do not change the state by get the updated state
updateState=currentState
return currentState
)
alert(updateState) // the current state.
)
【讨论】:
【参考方案13】:使用Background Timer 库。它解决了我的问题。
const timeoutId = BackgroundTimer.setTimeout(() =>
// This will be executed once after 1 seconds
// even when the application is the background
console.log('tac');
, 1000);
【讨论】:
【参考方案14】:关闭并不是唯一的原因。
基于useState
的源代码(简化如下)。在我看来,这个值永远不会被立即分配。
当您调用 setValue
时,会发生更新操作排队。在计划启动后,只有当您进入下一个渲染时,这些更新操作才会应用于该状态。
这意味着即使我们没有关闭问题,useState
的反应版本也不会立即为您提供新值。在下一次渲染之前,新值甚至都不存在。
function useState(initialState)
let hook;
...
let baseState = hook.memoizedState;
if (hook.queue.pending)
let firstUpdate = hook.queue.pending.next;
do
const action = firstUpdate.action;
baseState = action(baseState); // setValue HERE
firstUpdate = firstUpdate.next;
while (firstUpdate !== hook.queue.pending);
hook.queue.pending = null;
hook.memoizedState = baseState;
return [baseState, dispatchAction.bind(null, hook.queue)];
function dispatchAction(queue, action)
const update =
action,
next: null
;
if (queue.pending === null)
update.next = update;
else
update.next = queue.pending.next;
queue.pending.next = update;
queue.pending = update;
isMount = false;
workInProgressHook = fiber.memoizedState;
schedule();
还有一篇文章以类似的方式解释上述内容,https://dev.to/adamklein/we-don-t-know-how-react-state-hook-works-1lp8
【讨论】:
很好的分析。 Waaay 很少有人阅读源代码【参考方案15】:如果我们只需要更新状态,那么更好的方法是使用 push 方法。
这是我的代码。我想在状态中存储来自Firebase 的 URL。
const [imageUrl, setImageUrl] = useState([]);
const [reload, setReload] = useState(0);
useEffect(() =>
if (reload === 4)
downloadUrl1();
, [reload]);
const downloadUrl = async () =>
setImages([]);
try
for (let i = 0; i < images.length; i++)
let url = await storage().ref(urls[i].path).getDownloadURL();
imageUrl.push(url);
setImageUrl([...imageUrl]);
console.log(url, 'check', urls.length, 'length', imageUrl.length);
catch (e)
console.log(e);
;
const handleSubmit = async () =>
setReload(4);
await downloadUrl();
console.log(imageUrl);
console.log('post submitted');
;
此代码用于将 URL 置于数组状态。这也可能对您有用。
【讨论】:
【参考方案16】:在setter函数范围内实现状态设置回调(不带useEffect)并不复杂。
使用 promise 解决 setter 函数体中的新状态:
const getState = <T>(
setState: React.Dispatch<React.SetStateAction<T>>
): Promise<T> =>
return new Promise((resolve) =>
setState((currentState: T) =>
resolve(currentState);
return currentState;
);
);
;
这个例子展示了count
和outOfSyncCount
/syncCount
在UI渲染中的对比:
const App: React.FC = () =>
const [count, setCount] = useState(0);
const [outOfSyncCount, setOutOfSyncCount] = useState(0);
const [syncCount, setSyncCount] = useState(0);
const handleOnClick = async () =>
setCount(count + 1);
setOutOfSyncCount(count);
const newCount = await getState(setCount);
setSyncCount(newCount);
// Or then'able
// getState(setCount).then(setSyncCount);
;
return (
<>
<h2>Count = count</h2>
<h2>Synced count = syncCount</h2>
<h2>Out of sync count = outOfSyncCount</h2>
<button onClick=handleOnClick>Increment</button>
</>
);
;
我认为这种方法的一个吸引人的用例是使用与 Redux thunk 非常相似的上下文 api 和异步和/或副作用操作。这也将比Kent's method 涉及更少的复杂性。 (我没有在大型示例或生产中使用过这个,所以我不知道,也许你会进入一些奇怪的状态)。
例子:
// Sample "thunk" with async and side effects
const handleSubmit = async (
setState: React.Dispatch<SetStateAction<FormState>>
): Promise<void> =>
const state = await getState(setState);
if (!state.validated)
return;
// Submit endpoint
await fetch('submitFormUrl', method: 'post', body: state );
tackFormSuccess(state);
setState( ...state, submitted: true );
;
// Component consuming the thunk
const SubmitComponent: React.FC = () =>
const dispatch = useFormDispatch();
return <button onclick=() => handleSubmit(dispatch)>Submit</button>
【讨论】:
以上是关于useState 设置方法不会立即反映更改的主要内容,如果未能解决你的问题,请参考以下文章