【问题标题】:Same result on logging the state after render渲染后记录状态的结果相同
【发布时间】:2020-02-08 06:23:11
【问题描述】:

我知道 useState 是异步的。请在回答之前阅读完整的问题。

我正在尝试使用useState 修改数组中的一个元素,但它没有按预期工作。

数组和修改它的函数:

const [table, setTable] = useState(['blue', 'blue', 'blue', 'blue', 'blue']);

let currentShapePosition = 2;

function printTable() {
  let newTable = [...table];
  // let newTable = table;
  newTable[currentShapePosition] = 'red';
  setTable(newTable);
  console.log('printTable newTable', newTable); // <-- the result is as expected
  // log => printTable newTable ["blue", "blue", "red", "blue", "blue"]
  console.log('printTable table', table); // <--- The problem is here. I don't get why the array never update
  // log => printTable newTable ["blue", "blue", "blue", "blue", "blue"]
}

因为useState 是异步的,所以我知道数组可能不会立即更改,但是在printTable 函数内部,console.log 即使经过多次重新渲染,结果也是一样

当而不是: let newTable = [...table]

我这样做:let newTable = table

然后在函数中生成的console.log中更新状态,但没有重新渲染/组件更新。

我想了解为什么在第一种情况下newTable = [...table] 函数内部的console.log 结果在多次重新渲染后是相同的。 为什么在第二种情况下newTable = table 尽管setTable(newTable) 没有重新渲染组件。

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import "./style.css";

const App = () => {
  let currentShapePosition = 2;

  const [table, setTable] = useState(["blue", "blue", "blue", "blue", "blue"]);

  useEffect(() => {
    printTable();
    window.addEventListener("keydown", handleKeyPress);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  useEffect(() => {
    console.log("render", table);
  });

  function printTable() {
    let newTable = [...table];
    // let newTable = table;
    newTable[currentShapePosition] = "red";
    setTable(newTable);
    console.log("printTable newTable", newTable); // <-- the result is as expected
    console.log("printTable table", table); // <--- The problem is here. I don't get why the initial value never change
  }

  function handleKeyPress(event) {
    switch (event.key) {
      case "Left": // IE/Edge specific value
      case "ArrowLeft":
        moveShape(-1);
        break;
      case "Right": // IE/Edge specific value
      case "ArrowRight":
        moveShape(1);
        break;
      default:
        return;
    }
  }

  function moveShape(direction) {
    currentShapePosition += direction;
    printTable();
  }

  return (
    <table className="tetris-table">
      <tbody>
        <tr>
          <td className={table[0]} />
          <td className={table[1]} />
          <td className={table[2]} />
          <td className={table[3]} />
          <td className={table[4]} />
        </tr>
      </tbody>
    </table>
  );
};

const root = document.getElementById("root");

ReactDOM.render(<App />, root);

【问题讨论】:

  • 不是重复的,请大家看问题
  • 如果不是骗子,那就是缺少minimal reproducible example...
  • 它在代码沙箱中有一个例子,我相信它是因为闭包而发生的,但我承认我不是 100% 接受它。
  • 我在控制台中看到了这些值:printTable newTable ["blue", "blue", "red", "blue", "blue"] printTable table ["blue", "blue", "blue", "blue", "blue"] 渲染 ["blue", "blue", "red", "blue", "blue"] 我认为在第二种情况下它不应该总是 "blue" .

标签: javascript reactjs react-hooks setstate


【解决方案1】:

useState 调用是异步的,它们不会直接更新。 参考:https://reactjs.org/docs/hooks-state.html

在这个答案中阅读更多信息:useState set method not reflecting change immediately

【讨论】:

  • 我知道 useState 是异步的,因此数组可能不会立即更改,但是在 printTable 函数内部,即使在多次重新渲染后,console.log 结果也是一样的。实际上,即使在组件更新之后,例如 useEffect,在 print table 函数中,console.log 的结果也是一样的。
  • 请阅读这个问题,我投了反对票,因为它没有回答。
【解决方案2】:

问题是您的第一个useEffect,因为您删除了eslint 警告,它隐藏了一个潜在的错误。

useEffect(() => {
  printTable();
  window.addEventListener('keydown', handleKeyPress);
  //   v hidden bug, you should consider the warnings
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

这里发生的事情是,在 component mount 上,handleKeyPress第一个 实例分配给 addEventListener(请参阅 closures),其中所有数组都在价值“蓝色”,它会一直保持这种状态,直到卸载。

您应该知道每次渲染组件主体都会执行,因此在您的情况下,每个函数都有一个新实例。

currentPosition 也是如此,这应该是一个参考。

要修复它,请删除 eslint 评论并使用警告:

useEffect(() => {
  const printTable = () => {
    let newTable = [...table];
    newTable[currentPosition.current] = 'red';
    setTable(newTable);
    console.log('closure', table);
  };

  function handleKeyPress(event) {
    switch (event.key) {
      case 'Left': // IE/Edge specific value
      case 'ArrowLeft':
        moveShape(-1);
        break;
      case 'Right': // IE/Edge specific value
      case 'ArrowRight':
        moveShape(1);
        break;
      default:
        return;
    }
  }

  function moveShape(direction) {
    currentPosition.current += direction;
    printTable();
  }

  window.addEventListener('keydown', handleKeyPress);

  return () => window.removeEventListener('keydown', handleKeyPress);
}, [table]);

【讨论】:

  • 是的,也许。在问我的问题之前,我已经阅读了一些关于闭包的内容,但目前我还不太清楚闭包的工作方式。
  • 我发现了这个错误,它被认为是一个,如果有帮助,请随时接受答案。
  • 现在唯一缺少的是useEffect钩子的返回值,它应该是一个移除按键事件的函数。
  • return () =&gt; window.removeEventListener("keydown", handleKeyPress);
  • 谢谢,由于同样的原因,currentPosition 也应该有一个参考
【解决方案3】:

使用 React 方法创建的变量是具有特殊行为的特殊对象;)

您的函数引用变量table,它是一个特殊的 React(“状态”)对象。看起来,在这种情况下,函数总是获取变量的初始状态 - 否则它至少会在下一次渲染时显示不同的值。

如果您使用常规表作为变量,您将得到预期的结果。我认为您的代码的这个更改版本很好地证明了这一点:

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import "./style.css";

const App = () => {
  let currentShapePosition = 2;

  let tableDemo = ["blue", "blue", "blue", "blue", "blue"];
  function setTableDemo(newTable) {
    tableDemo = newTable.slice();
    setTable(tableDemo); // used to trigger rendering
  }

  // for rendering purposes (to keep the original code structure)
  const [table, setTable] = useState(tableDemo);

  useEffect(() => {
    printTable();
    window.addEventListener("keydown", handleKeyPress);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  useEffect(() => {
    console.log("render", table);
  });

  function printTable() {
    let newTable = [...table];
    // let newTable = table;
    newTable[currentShapePosition] = "red";
    setTableDemo(newTable);
    console.log("printTable newTable", newTable); // <-- the result is as expected
    console.log("printTable tableDemo", tableDemo); // <--- The problem is here. I don't get why the initial value never change
  }

  function handleKeyPress(event) {
    switch (event.key) {
      case "Left": // IE/Edge specific value
      case "ArrowLeft":
        moveShape(-1);
        break;
      case "Right": // IE/Edge specific value
      case "ArrowRight":
        moveShape(1);
        break;
      default:
        return;
    }
  }

  function moveShape(direction) {
    currentShapePosition += direction;
    printTable();
  }

  return (
    <table className="tetris-table">
      <tbody>
        <tr>
          <td className={table[0]} />
          <td className={table[1]} />
          <td className={table[2]} />
          <td className={table[3]} />
          <td className={table[4]} />
        </tr>
      </tbody>
    </table>
  );
};

const root = document.getElementById("root");

ReactDOM.render(<App />, root);


仅使用 React “状态”对象来保留“源状态值”

基本上最好只使用 React 的“状态”对象来保留“源状态值”,用于触发重新渲染。在这种特定情况下,源状态值为currentShapePosition。这样的表格不应该改变 - 只有这个表格的一些特定元素。所以实际上,根据 React 方法,代码可能看起来像这样:

import React, { useState, useEffect, useRef } from "react";
import ReactDOM from "react-dom";
import "./style.css";

const App = () => {
  const [currentPosition, setPosition] = useState(2);
  const table = useRef(["blue", "blue", "red", "blue", "blue"]);

  function handleKeyPress(event) {
    let newPosition;

    console.log("currentPosition:", currentPosition);
    table.current[currentPosition] = "blue";
    switch (event.key) {
      case "Left": // IE/Edge specific value
      case "ArrowLeft":
        newPosition = currentPosition - 1;
        break;
      case "Right": // IE/Edge specific value
      case "ArrowRight":
        newPosition = currentPosition + 1;
        break;
      default:
    }
    table.current[newPosition] = "red";
    console.log("newPosition:", newPosition);
    console.log("table:", table.current);
    // trigger the new render
    setPosition(newPosition);
  }

  useEffect(() => {
    window.addEventListener("keydown", handleKeyPress);

    return () => window.removeEventListener("keydown", handleKeyPress);
  });

  return (
    <table className="tetris-table">
      <tbody>
        <tr>
          <td className={table.current[0]} />
          <td className={table.current[1]} />
          <td className={table.current[2]} />
          <td className={table.current[3]} />
          <td className={table.current[4]} />
        </tr>
      </tbody>
    </table>
  );
};

const root = document.getElementById("root");

ReactDOM.render(<App />, root);


当然,最好的解决方案是动态渲染&lt;td&gt; 元素,带有相关类,没有任何表格。但这是一个不同的故事,除了这个问题。

【讨论】:

    猜你喜欢
    • 2016-04-22
    • 2019-10-30
    • 1970-01-01
    • 2021-04-24
    • 2022-06-30
    • 2020-12-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多