如何在React中从循环函数中调用包含延迟的函数时使用新的useState值?

huangapple go评论86阅读模式
英文:

How can I use a fresh useState value in a function called from a looped function including a timeout in React?

问题

In this code, I would like the output to print "Output:,1,2,3,4,5". But the output variable is stale because I iteratively call the function after a promise is returned from a closure.

What is an efficient way to keep a loop with this timed sleep effect and get my desired result?

英文:

In this code I would like the output to print "Output:,1,2,3,4,5"
But the output variable is stale because I iteratively call the function after a promise is returned from a closure.

What is an efficient way to keep a loop with this timed sleep effect, and get my desired result?

Code Sandbox: https://codesandbox.io/s/hidden-shape-6uh5jm?file=/src/App.js:0-1092

import "./styles.css";
import React, { useEffect, useState } from "react";

export default function App() {
  const [output, setOutput] = useState("Output: ");
  const [isLooping, setIsLooping] = useState(false);

  const sleep = (ms) => {
    return function (x) {
      return new Promise((resolve) => setTimeout(() => resolve(x), ms));
    };
  };

  const startLoop = () => {
    if (isLooping) {
      console.log("Click ignored - because is Looping");
      return;
    } else {
      setIsLooping(true);
      runLoop();
    }
  };

  const runLoop = (maxIterations = 0) => {
    setOutput(output + ", " + maxIterations);
    if (maxIterations >= 5) {
      setIsLooping(false);
      return;
    }
    sleep(1000)().then(() => {
      runLoop(maxIterations + 1);
    });
  };

  useEffect(() => {
    startLoop();
  }, []);

  return (
    <div className="App">
      <p>{output}</p>
      <button
        value="Start Loop"
        onClick={() => {
          startLoop();
        }}
      >
        Start loop
      </button>
      {isLooping && <p>Is Looping</p>}
    </div>
  );
}

答案1

得分: 0

以下是翻译好的部分:

任何时候 下一个 状态值依赖于 前一个 状态值,例如,像将值连接到前一个值的字符串连接,您将希望使用函数式状态更新

更新 runLoop 处理程序以使用函数式状态更新,这实际上是一个相当琐碎的更改:从 setOutput(output + ", " + maxIterations);setOutput(output => output + ", " + maxIterations);

const runLoop = (maxIterations = 0) => {
  setOutput(output => output + ", " + maxIterations);
  if (maxIterations >= 5) {
    setIsLooping(false);
    return;
  }
  sleep(1000)().then(() => {
    runLoop(maxIterations + 1);
  });
};

但是,这会产生奇怪的输出:"Output: , 0, 1, 2, 3, 4, 5"

这基本上是一个经典的栅栏问题

这是将要呈现的 UI 与要从中呈现的数据混合到状态中的结果。请记住,在React中,UI(例如,呈现的内容)是状态和属性的函数。React状态应该仅包含您需要存储的数据,而React组件将状态映射到呈现的UI。

这是一个示例,仅存储 maxIterations 值并从该状态呈现计算的输出字符串。

import "./styles.css";
import React, { useEffect, useState } from "react";

export default function App() {
  const [output, setOutput] = useState([]); // <-- 初始空数组
  const [isLooping, setIsLooping] = useState(false);

  const sleep = (ms) => {
    return function (x) {
      return new Promise((resolve) => setTimeout(() => resolve(x), ms));
    };
  };

  const startLoop = () => {
    if (isLooping) {
      console.log("点击被忽略 - 因为正在循环");
      return;
    } else {
      // 为下一次循环重置数组
      setOutput([]);
      setIsLooping(true);
      runLoop();
    }
  };

  const runLoop = (maxIterations = 0) => {
    setOutput((output) => output.concat(maxIterations)); // <-- 追加新值,返回新数组
    if (maxIterations >= 5) {
      setIsLooping(false);
      return;
    }
    sleep(1000)().then(() => {
      runLoop(maxIterations + 1);
    });
  };

  useEffect(() => {
    startLoop();
  }, []);

  return (
    <div className="App">
      <p>Output: {output.join(", ")}</p> // <-- 计算呈现的UI
      <button
        value="Start Loop"
        onClick={() => {
          startLoop();
        }}
      >
        Start loop
      </button>
      {isLooping && <p>Is Looping</p>}
    </div>
  );
}

现在呈现的输出将更符合预期:"Output: 0, 1, 2, 3, 4, 5"

演示

如何在React中从循环函数中调用包含延迟的函数时使用新的useState值?

英文:

Any time the next state value depends on the previous state value, e.g. like string concatenation of a value to the previous value, you will want to use a functional state update.

Update the runLoop handler to use a functional state update, it's really a rather trivial change: setOutput(output + &quot;, &quot; + maxIterations); to setOutput(output =&gt; output + &quot;, &quot; + maxIterations);.

const runLoop = (maxIterations = 0) =&gt; {
  setOutput(output =&gt; output + &quot;, &quot; + maxIterations);
  if (maxIterations &gt;= 5) {
    setIsLooping(false);
    return;
  }
  sleep(1000)().then(() =&gt; {
    runLoop(maxIterations + 1);
  });
};

This results in an odd output though: &quot;Output: , 0, 1, 2, 3, 4, 5&quot;

This is basically a classic fencepost problem.

This is the result of mixing the UI you want to render with the data you want to render it from, all into state. Keep in mind that in React, UI (e.g. what is rendered) is a function of state and props. The React state should contain just the data you need to store and the React component maps the state to the rendered UI.

Here's an example of storing just the maxIterations value and rendering the computed output string from that state.

import &quot;./styles.css&quot;;
import React, { useEffect, useState } from &quot;react&quot;;

export default function App() {
  const [output, setOutput] = useState([]); // &lt;-- initial empty array
  const [isLooping, setIsLooping] = useState(false);

  const sleep = (ms) =&gt; {
    return function (x) {
      return new Promise((resolve) =&gt; setTimeout(() =&gt; resolve(x), ms));
    };
  };

  const startLoop = () =&gt; {
    if (isLooping) {
      console.log(&quot;Click ignored - because is Looping&quot;);
      return;
    } else {
      // reset array for next looping
      setOutput([]);
      setIsLooping(true);
      runLoop();
    }
  };

  const runLoop = (maxIterations = 0) =&gt; {
    setOutput((output) =&gt; output.concat(maxIterations)); // &lt;-- append new value, return new array
    if (maxIterations &gt;= 5) {
      setIsLooping(false);
      return;
    }
    sleep(1000)().then(() =&gt; {
      runLoop(maxIterations + 1);
    });
  };

  useEffect(() =&gt; {
    startLoop();
  }, []);

  return (
    &lt;div className=&quot;App&quot;&gt;
      &lt;p&gt;Output: {output.join(&quot;, &quot;)}&lt;/p&gt; // &lt;-- compute rendered UI
      &lt;button
        value=&quot;Start Loop&quot;
        onClick={() =&gt; {
          startLoop();
        }}
      &gt;
        Start loop
      &lt;/button&gt;
      {isLooping &amp;&amp; &lt;p&gt;Is Looping&lt;/p&gt;}
    &lt;/div&gt;
  );
}

The rendered output will now be the more expected &quot;Output: 0, 1, 2, 3, 4, 5&quot;

Demo

如何在React中从循环函数中调用包含延迟的函数时使用新的useState值?

huangapple
  • 本文由 发表于 2023年5月11日 12:02:07
  • 转载请务必保留本文链接:https://go.coder-hub.com/76224071.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定