useState set 方法不会立即反映更改

IT技术 javascript reactjs react-hooks
2020-12-19 21:51:21

我正在尝试学习钩子,但该useState方法让我感到困惑。我正在为数组形式的状态分配一个初始值。in 的 set 方法useState对我不起作用,无论有没有扩展运算符。

我在另一台 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)

我希望将结果变量推送到电影数组中。

6个回答

很像继承React.Componentor创建的Class组件中的setState,React.PureComponent使用useStatehook提供的updater进行的状态更新也是异步的,不会立即反映出来。

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

即使您添加了一个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]));
嗨,在表单提交处理程序中调用 useState 怎么样?我正在验证一个复杂的表单,我在 submitHandler useState 钩子内部调用,不幸的是更改不是立即的!
2021-02-22 21:51:21
setMovies(prevMovies => ([...prevMovies, ...result])); 为我工作
2021-02-24 21:51:21
它记录了错误的结果,因为您正在记录一个陈旧的闭包,而不是因为 setter 是异步的。如果异步是问题,那么您可以在超时后登录,但您可以设置一个小时的超时并仍然记录错误的结果,因为异步不是导致问题的原因。
2021-02-25 21:51:21
请注意,虽然该建议非常好,但可以改进对原因的解释 - 与是否the updater provided by useState hook 是异步的事实无关,不像this.state如果this.setState是同步则可能会发生变异const movies即使useState提供了一个同步功能 - 请参阅我的回答中的示例
2021-02-27 21:51:21
useEffect不过可能不是最好的解决方案,因为它不支持异步调用。因此,如果我们想对movies状态更改进行一些异步验证,我们无法控制它。
2021-03-03 21:51:21

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

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

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

将其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>

@ACV 解决方案 2 适用于原始问题。如果您需要解决一个不同的问题,YMMW,但我仍然 100% 确定引用的代码按文档工作并且问题出在其他地方。
2021-02-12 21:51:21
@AlJoslin 乍一看,这似乎是一个单独的问题,即使它可能是由闭包范围引起的。如果您有具体问题,请使用代码示例和所有内容创建一个新的 stackoverflow 问题...
2021-02-14 21:51:21
实际上,我刚刚完成了使用 useReducer 的重写,遵循 @kentcdobs 文章(见下文),这确实给了我一个可靠的结果,并且没有受到这些闭包问题的影响。(参考:kentcdodds.com/blog/how-to-use-react-context-effectively
2021-02-15 21:51:21
优秀的答案。IMO,如果您投入足够的时间来阅读和吸收它,那么这个答案将在未来为您节省最多的心碎。
2021-02-17 21:51:21
这应该是“接受”的答案@Pranjal
2021-03-02 21:51:21

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

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

所以这个modulereact-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);
      }
    })();
  }, []);

更多信息:

我刚刚完成了使用 useReducer 的重写,遵循 @kentcdobs 文章(参考下面),这确实给了我一个可靠的结果,并且不受这些闭包问题的影响。

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

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

我不认为这个答案在 OP 的背景下特别有用。这个答案甚至没有使用useState()这是 OP 查询的核心。
2021-03-01 21:51:21
顺利的解决方案,但不是正在发生的事情的答案
2021-03-03 21:51:21

React useEffect 有自己的状态/生命周期,它与状态的变化有关,在效果被销毁之前不会更新状态。

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

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

当您使用 Promise API 时,最后一个代码示例既不需要 async 也不需要 await。那只需要在第一个
2021-02-22 21:51:21
伙计,你刚刚在午夜救了我!非常感谢@muhammad
2021-02-26 21:51:21