useState set 方法没有立即反映变化

The useState set method is not reflecting a change immediately

我正在尝试学习 hook,useState 方法让我感到困惑。我正在以数组的形式为状态分配一个初始值。 useState 中的 set 方法对我不起作用,无论是否使用扩展语法。

我在我正在调用的另一台 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) 都不起作用。

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

扩展React.ComponentReact.PureComponent创建的很多,使用useState hook提供的更新器的状态更新也是异步的,不会立即反映.

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

即使您添加了一个 setTimeout 函数,虽然超时将 运行 在重新渲染发生的一段时间后,setTimeout 仍将使用之前关闭的值而不是更新后的值。

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

如果你想对状态更新执行操作,你需要使用 useEffect 钩子,就像在 class 组件中使用 componentDidUpdate 自 setter useState 返回的没有回调模式

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

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

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

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

的其他详细信息:

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

TLDR:原因是 closure 范围围绕着不可变的 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都是assumed to be unchanging during 1 render

Treat this.state as if it were immutable.

有了钩子,这个假设通过使用 常量值const 关键字得到增强:

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

两次渲染之间的值可能不同,但在渲染本身和任何 closures 中保持不变(即使在渲染完成后寿命更长的函数,例如 useEffect,事件处理程序, 在任何 Promise 或 setTimeout 中)。

考虑跟随 fake,但是 同步,类似 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>

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

现在您应该看到,您的代码实际上确实 有效。不起作用的是 console.log(movies)。这是因为 movies 指向 旧状态 。如果将 console.log(movies) 移到 useEffect 之外,就在 return 上方,您将看到更新的电影对象。

我刚刚完成了对 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 = {};

现在我所有页面上的所有内容都可以顺利持续

React 的 useEffect 有自己的 state/lifecycle。跟state的突变有关,直到effect被销毁才会更新state。

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

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

我知道已经有很好的答案了。但我想给出另一个想法如何解决同样的问题,并使用我的模块 react-useStateRef.

访问最新的 'movie' 状态

正如您所了解的,通过使用 React 状态,您可以在每次状态更改时呈现页面。但是通过使用 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);
      }
    })();
  }, []);

更多信息:

我发现这很好。而不是将状态(方法 1)定义为,例如,

const initialValue = 1;
const [state,setState] = useState(initialValue)

试试这个方法(方法 2),

const [state = initialValue,setState] = useState()

这在不使用 useEffect 的情况下解决了重新渲染问题,因为在这种情况下我们不关心它的内部关闭方法。

P.S.: 如果您担心在任何用例中使用旧状态,则需要使用带有 useEffect 的 useState,因为它需要具有该状态,因此在这种情况下应使用方法 1 .

使用我的库中的自定义挂钩,您可以等待状态值更新:

  1. useAsyncWatcher(...values):watcherFn(peekPrevValue: boolean)=>Promise - 是 useEffect 的承诺包装器,它可以等待更新和 return 一个新值,如果可选的 peekPrevValue 参数设置为 true,则可能是前一个值。

(Live Demo)

    import React, { useState, useEffect, useCallback } from "react";
    import { useAsyncWatcher } from "use-async-effect2";
    
    function TestComponent(props) {
      const [counter, setCounter] = useState(0);
      const [text, setText] = useState("");
    
      const textWatcher = useAsyncWatcher(text);
    
      useEffect(() => {
        setText(`Counter: ${counter}`);
      }, [counter]);
    
      const inc = useCallback(() => {
        (async () => {
          await new Promise((resolve) => setTimeout(resolve, 1000));
          setCounter((counter) => counter + 1);
          const updatedText = await textWatcher();
          console.log(updatedText);
        })();
      }, []);
    
      return (
        <div className="component">
          <div className="caption">useAsyncEffect demo</div>
          <div>{counter}</div>
          <button onClick={inc}>Inc counter</button>
        </div>
      );
    }
    
    export default TestComponent;
  1. useAsyncDeepState 是一个深度状态实现(类似于 this.setState (patchObject)),其 setter 可以 return 与内部效果同步的承诺。如果不带参数调用 setter,它不会更改状态值,而只是订阅状态更新。在这种情况下,您可以从组件内部的任何位置获取状态值,因为函数闭包不再是障碍。

(Live Demo)

import React, { useCallback, useEffect } from "react";
import { useAsyncDeepState } from "use-async-effect2";

function TestComponent(props) {
  const [state, setState] = useAsyncDeepState({
    counter: 0,
    computedCounter: 0
  });

  useEffect(() => {
    setState(({ counter }) => ({
      computedCounter: counter * 2
    }));
  }, [state.counter]);

  const inc = useCallback(() => {
    (async () => {
      await new Promise((resolve) => setTimeout(resolve, 1000));
      await setState(({ counter }) => ({ counter: counter + 1 }));
      console.log("computedCounter=", state.computedCounter);
    })();
  });

  return (
    <div className="component">
      <div className="caption">useAsyncDeepState demo</div>
      <div>state.counter : {state.counter}</div>
      <div>state.computedCounter : {state.computedCounter}</div>
      <button onClick={() => inc()}>Inc counter</button>
    </div>
  );
}
var [state,setState]=useState(defaultValue)

useEffect(()=>{
   var updatedState
   setState(currentState=>{    // Do not change the state by get the updated state
      updateState=currentState
      return currentState
   })
   alert(updateState) // the current state.
})

使用Background Timer 库。它解决了我的问题。

const timeoutId = BackgroundTimer.setTimeout(() => {
    // This will be executed once after 1 seconds
    // even when the application is the background
    console.log('tac');
}, 1000);

关闭不是唯一的原因。

基于useState的源代码(下面进行了简化)。在我看来,该值从未立即分配。

当您调用 setValue 时,会发生更新操作排队的情况。在计划开始后,只有当您到达下一个渲染时,这些更新操作才会应用于该状态。

这意味着即使我们没有闭包问题,useState 的 React 版本也不会立即为您提供新值。新值甚至在下一次渲染之前都不存在。

  function useState(initialState) {
    let hook;
    ...

    let baseState = hook.memoizedState;
    if (hook.queue.pending) {
      let firstUpdate = hook.queue.pending.next;

      do {
        const action = firstUpdate.action;
        baseState = action(baseState);            // setValue HERE
        firstUpdate = firstUpdate.next;
      } while (firstUpdate !== hook.queue.pending);

      hook.queue.pending = null;
    }
    hook.memoizedState = baseState;

    return [baseState, dispatchAction.bind(null, hook.queue)];
  }

function dispatchAction(queue, action) {
  const update = {
    action,
    next: null
  };
  if (queue.pending === null) {
    update.next = update;
  } else {
    update.next = queue.pending.next;
    queue.pending.next = update;
  }
  queue.pending = update;

  isMount = false;
  workInProgressHook = fiber.memoizedState;
  schedule();
}

上面也有一篇文章用类似的方式解释,https://dev.to/adamklein/we-don-t-know-how-react-state-hook-works-1lp8

如果我们只需要更新状态,那么更好的方法是使用 push 方法。

这是我的代码。我想在 state.

中存储来自 Firebase 的 URL
const [imageUrl, setImageUrl] = useState([]);
const [reload, setReload] = useState(0);

useEffect(() => {
    if (reload === 4) {
        downloadUrl1();
    }
}, [reload]);


const downloadUrl = async () => {
    setImages([]);
    try {
        for (let i = 0; i < images.length; i++) {
            let url = await storage().ref(urls[i].path).getDownloadURL();
            imageUrl.push(url);
            setImageUrl([...imageUrl]);

            console.log(url, 'check', urls.length, 'length', imageUrl.length);
        }
    }
    catch (e) {
        console.log(e);
    }
};

const handleSubmit = async () => {
    setReload(4);
    await downloadUrl();
    console.log(imageUrl);
    console.log('post submitted');
};

此代码用于将 URL 置于数组状态。这可能也适合您。

不是说要这样做,但是在没有 useEffect 的情况下不难做到 OP 所要求的。

使用承诺解析 setter 函数体中的新状态:

const getState = <T>(
  setState: React.Dispatch<React.SetStateAction<T>>
): Promise<T> => {
  return new Promise((resolve) => {
    setState((currentState: T) => {
      resolve(currentState);
      return currentState;
    });
  });
};

这就是你如何使用它(示例显示 countoutOfSyncCount/syncCount 在 UI 渲染中的比较):

const App: React.FC = () => {
  const [count, setCount] = useState(0);
  const [outOfSyncCount, setOutOfSyncCount] = useState(0);
  const [syncCount, setSyncCount] = useState(0);

  const handleOnClick = async () => {
    setCount(count + 1);

    // Doesn't work
    setOutOfSyncCount(count);

    // Works
    const newCount = await getState(setCount);
    setSyncCount(newCount);
  };

  return (
    <>
      <h2>Count = {count}</h2>
      <h2>Synced count = {syncCount}</h2>
      <h2>Out of sync count = {outOfSyncCount}</h2>
      <button onClick={handleOnClick}>Increment</button>
    </>
  );
};

没有任何额外的 NPM 包

//...
const BackendPageListing = () => {
    
    const [ myData, setMyData] = useState( {
        id: 1,
        content: "abc"
    })

    const myFunction = ( x ) => {
        
        setPagenateInfo({
        ...myData,
        content: x
        })

        console.log(myData) // not reflecting change immediately

        let myDataNew = {...myData, content: x };
        
        console.log(myDataNew) // Reflecting change immediately

    }

    return (
        <>
            <button onClick={()=>{ myFunction("New Content")} }>Update MyData</button>
        </>
    )