开发者问题收集

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

2019-01-07
852026

我正在尝试学习钩子,但 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 数组中。

3个回答

与通过扩展 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]));
Shubham Khatri
2019-01-07

有关 上一个答案 的更多详细信息:

虽然 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()

但是, propsstate 都被 假定在 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>
Aprillion
2019-11-15

我知道已经有非常好的答案了。但我想给出另一个想法来解决同一问题,并使用我的模块 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);
      }
    })();
  }, []);

更多信息:

Aminadav Glickshtein
2020-12-24