乐闻世界logo
搜索文章和话题

React useState 设置值后没有立即生效?

9 个月前提问
5 个月前修改
浏览次数366

6个答案

1
2
3
4
5
6

当您在React的函数组件中使用useState钩子来设置一个状态值时,您可能会注意到,调用设置状态的函数后,状态的值不会立即改变。这是因为React中的状态设置是异步的。具体来说,这意味着React将在稍后某个时间点批量处理状态更新和重新渲染组件,而不是在调用设置状态函数的时候立即更新状态和重新渲染。

这样做有几个好处:

  1. 性能优化:通过合并多个状态更新,React可以减少不必要的渲染次数,从而避免额外的性能开销。
  2. 一致性保障:这样可以确保在一个事件处理函数中,不会因为状态更新导致其他状态的计算出现不一致的情况。

请看以下例子来说明这一点:

jsx
import React, { useState } from 'react'; function Counter() { const [count, setCount] = useState(0); const handleClick = () => { // 这里尝试连续更新状态,但状态的变化不会立即反映 setCount(count + 1); setCount(count + 1); // 在这个函数调用后,你可能会期待count是2,但是实际上它仍然是0 // 因为这两次更新会被合并,且基于相同的开始状态(0)计算 }; return ( <div> <p>Count: {count}</p> <button onClick={handleClick}>Increment</button> </div> ); }

在上面的例子中,当handleClick函数被调用时,即使我们调用了两次setCount来增加count,但在这个事件处理函数结束之前,你不会看到count的值发生变化。在实践中,如果你希望连续的状态更新彼此依赖,你应该使用函数形式的setState来确保每次更新都基于最新的状态:

jsx
setCount(previousCount => previousCount + 1);

使用函数形式的更新可以确保每次更新都基于上一次更新后的状态,而不是基于状态的闭包值。

2024年6月29日 12:07 回复

与通过扩展or创建的类组件非常相似.setState(),使用 hook 提供的更新程序进行的状态更新也是异步的,并且不会立即反映。React.Component``React.PureComponent``useState

此外,这里的主要问题不仅仅是异步性质,而是函数根据当前闭包使用状态值的事实,并且状态更新将反映在下一次重新渲染中,现有闭包不受影响,但新的闭包不会受到影响。那些被创建了。现在在当前状态下,钩子内的值是通过现有的闭包获取的,当重新渲染发生时,闭包会根据函数是否再次重新创建而更新。

即使您添加了一个setTimeout函数,尽管超时将在重新渲染发生一段时间后运行,但该函数setTimeout仍将使用其先前闭包中的值,而不是更新后的值。

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

如果要对状态更新执行操作,则需要使用钩子useEffect,就像componentDidUpdate在类组件中使用一样,因为返回的 setteruseState没有回调模式

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

就更新状态的语法而言,将用异步请求中可用的值替换状态中的setMovies(result)先前值。movies

但是,如果要将响应与先前存在的值合并,则必须使用状态更新的回调语法以及正确使用扩展语法,例如

shell
setMovies(prevMovies => ([...prevMovies, ...result]));
2024年6月29日 12:07 回复

先前答案的更多详细信息:

虽然 ReactsetState 是_异步的(类和钩子),并且很容易用这个事实来解释观察到的行为,但这并不是它发生的原因_。

TLDR:原因是围绕不可变值的闭包const范围。


解决方案:

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

    shell
    useEffect(() => { setMovies(result) }, []) console.log(movies)
  • 将变量添加到依赖项中(并使用react-hooks/exhaustive-deps eslint规则):

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

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

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

解释为什么会发生:

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

但是,假定propsstate两者在 1 render 期间保持不变

this.state就好像它是不可变的一样对待。

对于钩子,通过使用带有关键字的常量值来增强这种假设const

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

两次渲染之间的值可能不同,但在渲染本身和任何闭包内(即使在渲染完成后存活时间更长的函数,例如useEffect,任何 Promise 或 setTimeout 内的事件处理程序)仍然保持不变。

考虑以下假的、但同步的、类似 React 的实现:

shell
// 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>

运行代码片段隐藏结果

展开片段

2024年6月29日 12:07 回复

我知道已经有很好的答案了。但我想给出另一个想法如何解决同样的问题,并使用我的模块react-useStateRef访问最新的“电影”状态,它每周的下载量超过11,000次。

正如您所理解的,通过使用 React 状态,您可以在每次状态更改时渲染页面。但通过使用 React ref,您始终可以获得最新值。

因此,该模块react-useStateRef允许您一起使用状态和引用。它向后兼容React.useState,因此您只需替换该import语句即可

shell
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); } })(); }, []);

更多信息:

2024年6月29日 12:07 回复

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

只需在参数状态中传递一个参数或将其保留为黑色数组即可完美运行。

shell
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 hook 中进行即时更改。

shell
const movies = React.useRef(null); useEffect(() => { movies.current='values'; console.log(movies.current) }, [])
2024年6月29日 12:07 回复

这里的大多数答案都是关于如何根据其先前的值更新状态,但我不明白这与问题有何关系

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


反应18

useState 是异步的:

当触发某个代码的事件发生时,代码开始运行,当它完成时,react将检查是否有状态更新,如果是,则只有useState更新钩子的值,这会导致一个新的渲染,其中新值可用。

shell
const [example,setExemple] = useState("") //... <button onClick={() => { const newValue = "new"; setExample(newValue); console.log(example); // output "" and this is normal, because the component didn't rerenderd yet so the new value is not availabe yet }} > Update state </button>

假设我们有一个场景,其中一个状态依赖于另一个状态,例如我们希望根据example每次更新时的新值进行 API 调用,然后将响应中的数据存储在另一个状态中anotherExample
为了实现这样我们有两种方法:

**1. 使用 的值newValue
**

shell
<button onClick={async () => { const newValue = "new"; const response = await axios.get(`http://127.0.0.1:5000/${newValue}`); setExample(newValue); setAnotherExample(response.data); }} > test </button>

因为您知道example将收到该值,所以您可以直接基于它创建逻辑。

2.每次更新时触发useEffect运行,方法是将其包含在其依赖项数组中:example``example

shell
<button onClick={() => { const newValue = "new"; setExample(newValue); }} > test </button> useEffect(() => { async function test(){ const response = await axios.get(`http://127.0.0.1:5000/${example}`); setAnotherExample(response.data); } test(); }, [example])

因此,当example使用事件函数更新组件重新渲染时,我们现在处于一个新的不同渲染中,一旦完成,useEffect就会运行**,因为 的值example与上次渲染期间的值不同**,并且因为它是一个新的不同渲染, useState 挂钩的新值example可在此处获得。

注意:useEffect在第一次安装期间,钩子无论如何都会运行。

哪种方法更好?

  • 而第一种方法将使所有工作在一次渲染中完成**(更好的方法)** “React 将多个状态更新分组到单个重新渲染中以获得更好的性能”,第二种方法将在两次渲染中完成,第一次example更新时和第二个anotherExample是从内部更新的useEffect😕

  • 由于组件仅在useState钩子的新值与旧值不同时才重新渲染,因此当newValue等于 时example组件不会重新渲染,因此useEffect不会运行且anotherExample不会更新 🙂 (更好的方法),但是在第一个中无论如何,API 都会被调用,如果没有必要,我们不想这样做,如果发生这种情况,anotherExample也会更新(anotherExample将收到它已经包含的相同数据,因为它是相同的请求,因为newValue等于example)但是如果然后,在对象或数组中响应,Object.is方法(钩子useState使用的)无法检测新值是否等于前一个值,因此,组件将重新渲染😕

结论:

正如上面提到的,每种都有其优点,所以这取决于用例。

更推荐使用第二种方法,但是在某些情况下,第一种方法的性能更高,例如,当您确定代码仅在newValue使用 获取新值时才会运行onChange,或者当您想使用您将要使用的其他一些局部变量时不再能够从 useEffect 内部访问

2024年6月29日 12:07 回复

你的答案