useState 设置方法没有立即反映变化

2024-11-02 21:00:00
admin
原创
33
摘要:问题描述:我正在尝试学习 hooks,但该useState方法让我感到困惑。我正在以数组的形式为状态分配初始值。useState无论是否使用扩展语法,set 方法对我来说都不起作用。我在另一台电脑上创建了一个 API,我正在调用它并获取我想要设置到状态中的数据。这是我的代码:<div id=&a...

问题描述:

我正在尝试学习 hooks,但该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>

运行代码片段Hide results展开片段

setMovies(result)不起作用setMovies(...result)

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


解决方案 1:

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

此外,这里的主要问题不仅仅是异步性,还在于函数根据其当前闭包使用状态值,并且状态更新将反映在下一次重新渲染中,现有闭包不受影响,但会创建新的闭包。现在在当前状态下,钩子中的值由现有闭包获取,当重新渲染发生时,闭包将根据函数是否再次重新创建进行更新。

即使你添加了一个setTimeout函数,虽然重新渲染发生之后的一段时间内会运行超时,但它setTimeout仍然会使用先前闭包中的值,而不是更新的值。

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

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

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

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

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

setMovies(prevMovies => ([...prevMovies, ...result]));

解决方案 2:

上一个答案的附加详细信息:

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

TLDR:原因在于围绕不可变值的闭包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()

然而,在 1 次渲染期间,props和都state被假定为保持不变。

this.state其视为不可变的。

使用钩子,可以通过使用带有关键字的常量值来增强此假设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>

运行代码片段Hide results展开片段

解决方案 3:

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

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

因此,该模块react-useStateRef允许您同时使用状态和引用。它向后兼容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-usestateref

解决方案 4:

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

useState 设置方法没有立即反映变化


React 18

useState 是异步的:

当发生触发特定代码的事件时,代码开始运行,当它完成时,React 将检查是否存在状态更新,如果是,则仅更新钩子的值useState,并且这会导致新的渲染,其中新值可用。

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

<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

<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无论如何都会运行。

哪种方法更好?

  • 虽然第一种方法将在一次渲染中完成所有工作

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   601  
  华为IPD与传统研发模式的8大差异在快速变化的商业环境中,产品研发模式的选择直接决定了企业的市场响应速度和竞争力。华为作为全球领先的通信技术解决方案供应商,其成功在很大程度上得益于对产品研发模式的持续创新。华为引入并深度定制的集成产品开发(IPD)体系,相较于传统的研发模式,展现出了显著的差异和优势。本文将详细探讨华为...
IPD流程是谁发明的   7  
  如何通过IPD流程缩短产品上市时间?在快速变化的市场环境中,产品上市时间成为企业竞争力的关键因素之一。集成产品开发(IPD, Integrated Product Development)作为一种先进的产品研发管理方法,通过其结构化的流程设计和跨部门协作机制,显著缩短了产品上市时间,提高了市场响应速度。本文将深入探讨如...
华为IPD流程   9  
  在项目管理领域,IPD(Integrated Product Development,集成产品开发)流程图是连接创意、设计与市场成功的桥梁。它不仅是一个视觉工具,更是一种战略思维方式的体现,帮助团队高效协同,确保产品按时、按质、按量推向市场。尽管IPD流程图可能初看之下显得错综复杂,但只需掌握几个关键点,你便能轻松驾驭...
IPD开发流程管理   8  
  在项目管理领域,集成产品开发(IPD)流程被视为提升产品上市速度、增强团队协作与创新能力的重要工具。然而,尽管IPD流程拥有诸多优势,其实施过程中仍可能遭遇多种挑战,导致项目失败。本文旨在深入探讨八个常见的IPD流程失败原因,并提出相应的解决方法,以帮助项目管理者规避风险,确保项目成功。缺乏明确的项目目标与战略对齐IP...
IPD流程图   8  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用