useState set方法没有立即反映更改

我正在尝试学习钩子,而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)都不起作用。

我希望将result变量推送到movies数组中。

594643 次浏览

通过扩展React.ComponentReact.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值。


解决方案:

  • 读取渲染函数中的值(不在嵌套函数中):

      useEffect(() => { setMovies(result) }, [])
    console.log(movies)
    
  • 将变量添加到依赖项中(并使用反应-钩子/穷举-de ps eslint规则):

      useEffect(() => { setMovies(result) }, [])
    useEffect(() => { console.log(movies) }, [movies])
    
  • 使用临时变量:

      useEffect(() => {
    const newMovies = result
    console.log(newMovies)
    setMovies(newMovies)
    }, [])
    
  • 使用可变引用(如果我们不需要状态并且只想记住值-更新ref不会触发重新渲染):

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

解释为什么会发生:

如果异步是唯一的原因,那么await setState()是可能的。

但是,propsstate都是假设在1次渲染期间不变

this.state视为不可变。

使用钩子,通过使用常量值const关键字来增强这个假设:

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

2个渲染之间的值可能不同,但在渲染本身和任何关闭(即使渲染完成后寿命更长的函数,例如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>

// replace
return <p>hello</p>;
// with
return <p>{JSON.stringify(movies)}</p>;

现在你应该看到,你的代码实际上确实有效。不起作用的是console.log(movies)。这是因为movies指向旧州。如果你将console.log(movies)移到useEffect之外,就在返回值的正上方,你将看到更新的电影对象。

我刚刚完成了一个使用useReduce er的重写,下面是@kentcdobs的文章(参考如下),它确实给了我一个可靠的结果,没有受到这些闭包问题的影响。

请参阅:https://kentcdodds.com/blog/how-to-use-react-context-effectively

我将他可读的样板压缩到我喜欢的干燥程度-阅读他的沙盒实现将向您展示它的实际工作原理。

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 = {};

现在一切都在我所有的页面上顺利地持续着

React的用效有自己的状态/生命周期。它与状态突变有关,直到效果被破坏后才会更新状态。

只需在参数状态下传递一个参数或留下一个黑色数组,它就会完美地工作。

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()在React钩子中进行即时更改。

const movies = React.useRef(null);
useEffect(() => {
movies.current='values';
console.log(movies.current)
}, [])

我知道已经有了很好的答案。但我想给出另一个想法,如何解决同样的问题,并使用我的模块响应状态访问最新的“电影”状态。

正如你所理解的,通过使用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);
}
})();
}, []);

更多信息:

我发现这很好。而不是将状态(方法1)定义为,例如,

const initialValue = 1;
const [state,setState] = useState(initialValue)


尝试这种方法(方法2),

const [state = initialValue,setState] = useState()


这解决了重新渲染问题而不使用useEffects,因为我们不关心它在这个案例中的内部关闭方法。

P. S.:如果您关心在任何用例中使用旧状态,则需要使用带有useEffects的useState,因为它需要具有该状态,因此在这种情况下应使用方法1。

使用我的库中的自定义挂钩,您可以等待状态值更新:

  1. useAsyncWatcher(...values):watcherFn(peekPrevValue: boolean)=>Promise-是一个围绕useEffects的承诺包装器,它可以等待更新并返回一个新值,如果可选的peekPrevValue参数设置为true,则可能返回前一个值。

直播演示

    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;
  1. useAsyncDeepState是一个深度状态实现(类似于this.setState(patchObject)),其setter可以返回与内部特效同步的Promise。如果没有参数调用setter,它不会更改状态值,而是简单地订阅状态更新。在这种情况下,您可以从组件内部的任何地方获取状态值,因为函数闭包不再是障碍。

直播演示

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>
);
}
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.
})

使用背景定时器库。它解决了我的问题。

const timeoutId = BackgroundTimer.setTimeout(() => {
// This will be executed once after 1 seconds
// even when the application is the background
console.log('tac');
}, 1000);

关闭并不是唯一的原因。

基于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

如果我们必须只更新状态,那么更好的方法是如果我们使用推送方法来这样做。

这是我的代码。我想将来自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作为数组置于状态。这可能也适用于您。

不是说要这样做,但在没有使用效果的情况下做OP要求的事情并不难。

使用承诺来解析setter函数主体中的新状态:

const getState = <T>(
setState: React.Dispatch<React.SetStateAction<T>>
): Promise<T> => {
return new Promise((resolve) => {
setState((currentState: T) => {
resolve(currentState);
return currentState;
});
});
};

这就是你如何使用它(示例显示了UI渲染中countoutOfSyncCount/syncCount之间的比较):

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);


// Doesn't work
setOutOfSyncCount(count);


// Works
const newCount = await getState(setCount);
setSyncCount(newCount);
};


return (
<>
<h2>Count = {count}</h2>
<h2>Synced count = {syncCount}</h2>
<h2>Out of sync count = {outOfSyncCount}</h2>
<button onClick={handleOnClick}>Increment</button>
</>
);
};

没有任何附加的NPM包

//...
const BackendPageListing = () => {
    

const [ myData, setMyData] = useState( {
id: 1,
content: "abc"
})


const myFunction = ( x ) => {
        

setPagenateInfo({
...myData,
content: x
})


console.log(myData) // not reflecting change immediately


let myDataNew = {...myData, content: x };
        

console.log(myDataNew) // Reflecting change immediately


}


return (
<>
<button onClick={()=>{ myFunction("New Content")} }>Update MyData</button>
</>
)

我也遇到了同样的问题。正如上面的其他答案已经澄清了这里的错误,那就是useState是异步的,你正试图在setState之后使用该值。由于setState的异步性质,它没有在console.log()部分上更新,它允许你的进一步代码执行,而值更新在后台进行。因此你得到了以前的值。当setState在后台完成时,它将更新该值,你将在下一次渲染时访问该值。

如果有人有兴趣详细了解这一点。这里有一个关于这个主题的非常好的会议演讲。

https://www.youtube.com/watch?v=8aGhZQkoFbQ

我想立即更新状态的值以将其保存在DB中,我来回答这个问题。调用更新程序只是为了获得最新的值。

const [pin, setPin] = useState(0);
setPin(pin ? 0 : 1);
setPin((state) => {
console.log(state); // 1
//Here I called the DB method and passed the state as param in mehtod
return state;
});