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

819

我正在尝试学习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>

setMovies(result)setMovies(...result)都无法正常工作。

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


你能看到将 console.log("movies =", movies); 移到 useEffect 钩子之外的更改吗? - Domenico Ruggiano
19个回答

2
如果我们只需要更新状态,那么更好的方法是使用push方法来实现。以下是我的代码。我想将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以数组的形式存储在状态中。这对你也可能有用。

2
.push会改变当前状态,这是React中的不良实践。以下是更新状态数组的正确方法 - Emile Bergeron
在循环中调用 setImageUrl 是另一种不好的做法,因为它是异步调用(在 React 生命周期之外),不会批处理每次调用时触发的新渲染。正确的方法应该是构建新数组,然后仅调用一次 setImageUrl - Emile Bergeron
此外,在循环中使用 await 是低效的(参见 https://eslint.org/docs/rules/no-await-in-loop)。像 Promise.all 这样的方法可以改善这种情况。 - Emile Bergeron

0
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.
})

1
不要那样做。setState的setter回调应该是纯函数。而且在这里,updatedState将始终为undefined - Emile Bergeron
@EmileBergeron,你有没有文档链接说明回调函数应该没有副作用? - Ricola
我手头没有链接,但它是与严格模式一起记录的,有助于识别不需要的副作用。 - Emile Bergeron

0

不需要任何额外的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>
        </>
    )

0

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

  1. useAsyncWatcher(...values):watcherFn(peekPrevValue: boolean)=>Promise - 是一个围绕 useEffect 的 Promise 包装器,可以等待更新并返回一个新值,如果可选的 peekPrevValue 参数设置为 true,则可能还会返回前一个值。

(演示)

    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;
  • useAsyncDeepState 是一个深度状态实现(类似于 this.setState(patchObject)),其 setter 可以返回与内部 effect 同步的 promise。如果没有参数调用 setter,则不会更改状态值,而只是订阅状态更新。在这种情况下,您可以从组件内的任何位置获取状态值,因为函数闭包不再是障碍。
  • (演示)

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

    0

    ⚠️ 功能性组件

    我相信一个非常干净的方法是创建一个自定义钩子,它提供了将回调函数传递给设置函数的能力,这样就可以确保在状态更新后立即执行某些操作。

    通过查看这篇文章,您可以了解如何创建useStateCallback钩子。使用useStateCallback定义状态的示例如下:

    const [count, setCount] = useStateCallback(0);
    
    const handleFooBar = () => {
      setCount(c => c + 1, () => { // The callback function
        // All actions here will be run exactly AFTER the update of the count state
      })
    };
    

    0
    问题
    我的问题实际上并不是在调用set方法后立即尝试访问状态。我是在重新渲染后的完全不同的函数中尝试这样做,但更新仍然没有反映出来。目标函数在一个函数组件中定义,但是它是从一个类组件中调用的。
    在我的情况下,我最终意识到这是由于闭包过期引起的问题。这可能是因为类组件不使用useState功能,所以我的代码中的类组件接受传递给它的函数,并创建了一个副本或其他东西,而该副本没有使用最新的变量引用。然而,直接传递给类组件的实际变量仍然正确地反映出来。
    解决方案
    将类组件替换为函数组件解决了我的问题。

    -2

    并不是说要这样做,但是在不使用 useEffect 的情况下实现 OP 所要求的功能并不难。

    可以使用 Promise 在 setter 函数的主体中解析新状态:

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

    这是如何使用它的方法(示例显示了 UI 渲染中 countoutOfSyncCount/syncCount 之间的比较):

    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>
        </>
      );
    };
    

    3
    setState 的设置器回调应该是pure function - Emile Bergeron
    1
    我赞成强调纯函数,但是那个链接并不特定于useState回调函数(有时候不使用纯函数作为回调函数也是有用的)。此外,虽然这不是“纯”的,但它实际上也没有改变任何状态。我并不是说这是最好的方法(我不使用它),只是它提供了一个替代方案来回答OP的问题。 - Tunn
    2
    使用 count 和回调函数会阻碍 useState 自身的设计方法,我很担心。 - Jamie Nicholl-Shelley
    1
    再次强调,这并不是说这是最好的方法或者建议使用它,只是说它能够工作。更有用的做法是提供一个可重现的例子来说明使用它可能会遇到哪些问题,而不是从设计理论层面将其忽略掉。 - Tunn
    1
    “设计理论层面”似乎是批评某些事情的完全合法的理由。仅仅因为某些东西可行或可能可行,并不意味着值得推广。人们可能会忽略警告而使用这种模式。有更好的方法来处理这个“问题”(实际上,它本来就不是问题,只是对于不习惯异步代码的人来说,看起来像是问题)。 - ggorlen
    1
    不是不同意你的观点。我的观点是,没有人给出具体的例子或解释,说明这里的警告或缺点是什么,或者提供一个链接,显示这与设计不一致。我可以接受被踩,但我认为像“有更好的方法”这样的评论并不特别有见地,即使它可能是完全有效的,也请展示或链接到一些东西 :) - Tunn

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

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


    8
    不确定为什么这个答案被严重投票否决,它是通过将 console.log 放在 useState 函数外部来说明如何获得“预期”的值。简单明了,如果有人想知道为什么会发生这种情况,可以参考上面的详细说明。 - vikramvi
    虽然很努力,但还需加油。 - ADITYA AHLAWAT

    -4
    使用 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);
    

    2
    添加延迟不是实际解决方案,只是一种权宜之计。即使这样,当你可以使用简单的setTimeout时,也不需要使用库。 - Prabhu Thomas

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接