useState设置方法不能立即反映更改


163

我正在尝试学习钩子,该useState方法使我感到困惑。我正在将初始值分配给数组形式的状态。useState即使使用spread(...)或,in中的set方法对我也不起作用without spread operator。我已经在另一台PC上创建了一个API,我正在调用该API并提取要设置为状态的数据。

这是我的代码:

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

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);
        setMovies(result);
        console.log(movies);
      } catch (e) {
        console.error(e);
      }
    })();
  }, []);

  return <p>hello</p>;
};

const rootElement = document.getElementById("root");
ReactDOM.render(<StateSelector />, rootElement);

setMovies(result)以及setMovies(...result)不工作。可以在这里使用一些帮助。

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

Answers:


217

类似于通过扩展React.Component或创建的Class组件中的setState,React.PureComponent使用useState钩子提供的更新程序的状态更新也是异步的,不会立即反映出来

同样,这里的主要问题不仅是异步性质,而且函数根据状态的当前闭包和状态更新使用状态值这一事实将反映在下一次重新渲染中,现有的闭包不会受到影响,而是会创建新的闭包。现在,在当前状态下,挂钩中的值是由现有的闭包获得的,并且在重新渲染时,将根据是否再次创建函数来更新闭包。

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

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

17
嗨,在表单提交处理程序中调用useState怎么样?我正在验证一个复杂的表单,我在SubmitHandler useState钩子内部调用,不幸的是更改不是立即进行的!
da45

2
useEffect但是,由于它不支持异步调用,因此可能不是最佳解决方案。因此,如果我们想对movies状态更改进行一些异步验证,则无法对其进行控制。
RA。

2
请注意,尽管建议非常好,但是可以改善原因的解释-与是否the updater provided by useState hook 异步无关,与之不同的是,this.state如果this.setState同步可能会被更改,即使发生闭包,const movies即使useState提供了同步功能-请参见我的答案示例
4

setMovies(prevMovies => ([...prevMovies, ...result]));为我工作
Mihir

它记录错误的结果是因为您记录的是过时的关闭,而不是因为setter是异步的。如果问题是异步的,那么您可以在超时后进行日志记录,但是您可以将超时时间设置为一个小时,但仍然记录错误的结果,因为异步并不是导致问题的原因。
HMR

126

上一个答案的其他详细信息:

尽管React setState 异步的(类和钩子),并且很想用该事实来解释观察到的行为,但这并不是它发生的原因

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


解决方案:

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

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

    useEffect(() => { setMovies(result) }, [])
    useEffect(() => { console.log(movies) }, [movies])
  • 使用可变引用(无法执行上述操作时):

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

解释为什么会发生:

如果异步是唯一原因,则有可能await setState()

但是,都假定propsstate在1 render期间不变

对待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>


11
极好的答案。IMO,如果您花足够的时间阅读和吸收它,那么此答案将为您节省将来最大的伤痛。
Scott Mallon

这使得将上下文与路由器结合使用非常困难,是吗?当我进入下一页时,状态消失了。而且我不能仅在useEffect挂钩内设置状态,因为它们无法呈现...我很欣赏答案的完整性,它解释了我所看到的,但是我无法弄清楚如何击败它。我要在上下文中传递函数以更新无法有效持久保存的值...所以我必须将它们从上下文中的闭包中取出,是吗?
Al Joslin

乍一看,@ AlJoslin似乎是一个单独的问题,即使它可能是由闭包范围引起的。如果您有一个具体的问题,请创建一个新的stackoverflow问题,包括代码示例和所有...
Aprillion

1
实际上,在@kentcdobs文章(参见下面的参考文献)之后,我刚刚完成了useReducer的重写,这确实给了我一个可靠的结果,而这些问题一点也不遭受这些闭合问题的困扰。(ref:kentcdodds.com/blog/how-to-to-use-react-context-effectively
乔斯林

1

在@kentcdobs文章(参见下面的参考文献)之后,我刚刚完成了useReducer的重写,这确实给了我一个可靠的结果,这些问题一点也不遭受这些关闭问题的困扰。

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

现在,所有页面上的所有内容都可以顺利进行

真好!

谢谢肯特!


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

现在您应该看到,您的代码确实有效。什么是行不通的console.log(movies)。这是因为movies指向旧状态。如果将移到console.log(movies)之外useEffect,则在退货单的正上方,您将看到更新的电影对象。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.