【问题标题】:Does useState hook change the value of the stateuseState 钩子是否改变了状态的值
【发布时间】:2019-11-16 18:19:11
【问题描述】:

我刚开始 React,在这个项目列表教程中我有一些关于更新项目状态的问题。另外,我正在使用功能组件。所以在 app.js 中

const [items, setItems] = useState([
  {
    id: 1,
    title: 'Banana',
    bought: false
  },
 ...
])

然后我在 app.js 中有一个函数可以在我选中一个框时将购买的内容更新为真或假

// The id is passed in from Item.js down below
const markBought = (id) => {
  setItems(
    items.map(
     item => {
      if (item.id === id) {
        /// If bought is false, checking it will make it true and vice versa
        item.bought = !item.bought; // (1)
      }
      return item; // (2)
    })
  );
};
return (
  <div className="App">
    <Items items={items} markBought={markBought}></Items>
  </div>
);

老师说我们正在使用一种叫做组件钻孔的东西。所以在Items.js中,我们通过映射每一个item来一个一个的展示出来,但是我觉得没必要展示出来。

终于在 Item.js 中

<input type="checkbox" onChange={() => props.markBought(props.item.id)} />
{props.item.title}

该应用程序运行良好,但对我来说有点混乱。所以:

  1. 在app.js中,我们改变购买状态后,是不是也需要退货,和条件为假时退货一样?为什么说错了才退货,对了才改不退货?
  2. 我读到 map 不会修改数组,所以 markBought 函数应该创建一个新的 items 数组,已经修改了买的,但是这个数组会发生什么,React 怎么知道将这个“道具”到 item.js,而不是我硬编码的那些?

对不起,如果这有点长,任何帮助将不胜感激。感谢阅读

【问题讨论】:

  • 欢迎堆栈溢出!通常最好一次只针对一个问题发布您的帖子。关于您的第一个问题,无论条件是否为真,return item 行都会被调用。也许你会感到困惑,如果你有 ifelse 包装你的回报,但这段代码没有......你的第二个问题最好通过阅读官方反应文档来回答 reactjs.org/docs/state-and-lifecycle.html
  • 对于您的第一个问题,无论是否满足 if 条件,您的 map 函数始终返回该项目。只有在 if 中放入另一个 return 语句,执行才会停止。
  • 您使用 map 的方式正在改变数组,您不应该这样做。如果您想使用 map 更改数组中的一项,您应该复制该一项并更改该副本,简短的语法是扩展运算符:copy = {...original: changed:'other value'} 对于一个完整的工作示例,您可以查看我的答案并评论它,如果你有任何问题。

标签: reactjs


【解决方案1】:

您正在改变地图中的项目,如果您将 Item 组件优化为纯组件,那么该组件将不会因为突变而重新渲染。请尝试以下方法:

//use useCallback so marBought doesn't change and cause
//  needless DOM re renders
const markBought = useCallback(id => {
  setItems((
    items //pass callback to the setter from useState
  ) =>
    items.map(
      item =>
        item.id === id
          ? { ...item, bought: !item.bought } //copy item with changed value
          : item //not this item, just return the item
    )
  );
}, []);

这是一个完整的例子:

const { useCallback, useState, useRef, memo } = React;
function Items() {
  const [items, setItems] = useState([
    {
      id: 1,
      title: 'Banana',
      bought: false,
    },
    {
      id: 2,
      title: 'Peach',
      bought: false,
    },
  ]);
  const toggleBought = useCallback(id => {
    setItems((
      items //pass callback to the setter from useState
    ) =>
      items.map(
        item =>
          item.id === id
            ? { ...item, bought: !item.bought } //copy item with changed value
            : item //not this item, just return the item
      )
    );
  }, []);
  return (
    <div>
      {items.map(item => (
        <Item
          key={item.id}
          item={item}
          toggleBought={toggleBought}
        />
      ))}
    </div>
  );
}
//use memo to make Item a pure component
const Item = memo(function Item({ item, toggleBought }) {
  const renderedRef = useRef(0);
  renderedRef.current++;
  return (
    <div>
      <div>{item.title}</div>
      <div>bought: {item.bought ? 'yes' : 'no'}</div>
      <button onClick={() => toggleBought(item.id)}>
        toggle bought
      </button>
      <div>Rendered: {renderedRef.current} times</div>
    </div>
  );
});

//render the application
ReactDOM.render(<Items />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

这是一个损坏的示例,您改变了项目并且即使状态确实发生了变化也不会看到重新渲染:

const { useCallback, useState, useRef, memo } = React;
function Items() {
  const [items, setItems] = useState([
    {
      id: 1,
      title: 'Banana',
      bought: false,
    },
    {
      id: 2,
      title: 'Peach',
      bought: false,
    },
  ]);
  const toggleBought = useCallback(id => {
    setItems((
      items //pass callback to the setter from useState
    ) =>
      items.map(
        item =>
          item.id === id
            ? ((item.bought = !item.bought),item) //mutate item
            : item //not this item, just return the item
      )
    );
  }, []);
  return (
    <div>
      <div>
        {items.map(item => (
          <Item
            key={item.id}
            item={item}
            toggleBought={toggleBought}
          />
        ))}
      </div>
      <div>{JSON.stringify(items)}</div>
    </div>
  );
}
//use memo to make Item a pure component
const Item = memo(function Item({ item, toggleBought }) {
  const renderedRef = useRef(0);
  renderedRef.current++;
  return (
    <div>
      <div>{item.title}</div>
      <div>bought: {item.bought ? 'yes' : 'no'}</div>
      <button onClick={() => toggleBought(item.id)}>
        toggle bought
      </button>
      <div>Rendered: {renderedRef.current} times</div>
    </div>
  );
});

//render the application
ReactDOM.render(<Items />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

【讨论】:

  • 你好。谢谢你的热情。为什么在这种情况下改变原始数组是不好的?我认为 map 总是返回带有修改值的新数组,但在这种情况下,你的意思是它实际上改变了原始值,而不是像它应该的那样创建新数组吗?这些代码来自教程本身,我刚开始,所以你说的对我来说有点难。但再次非常感谢您。
  • @GoldExperience 你不应该在 React 中改变本地状态或 redux 状态。当你变异时,React 不会检测到状态变化,我在答案中添加了一个损坏的(变异)示例,你会看到它损坏了。 Map 确实返回一个新的 array 但指向相同的项目并且您改变了该项目。如果这是来自教程,那么我建议使用另一个教程,并可能通知作者错误。
【解决方案2】:

您好,欢迎来到 Stackoverflow。

  1. 您总是返回item。您只需有一个 if 语句将更改购买状态,即使上述条件为假,item 也会返回,这是正确的做法。

  2. Map 确实不会修改数组,而是返回一个新数组。如果你想得到那个返回数组,你可以简单地做:

const myNewArray = items.map(...)

这个新的array 到达您的其他组件的方式是因为这个新的array 被分配给了您的useState()。 你看到setItems() 了吗?这将设置您的状态,Item.js 将自动更新。 这就是反应的伟大之处。更新此state 后,将更新从state 提供的所有组件。

【讨论】:

  • 这不是正确的做法,正确的做法是 items.map( item =&gt; item.id === id ? { ...item, bought: !item.bought } : item ) OP 使用 map 的方式,项目将被变异 item.bought = !item.bought 将改变 map 返回的数组和原始数组 ( modified 是您想要的任何一种方式,但不是突变)。您提出的唯一有效点是 map 将返回一个新数组,但是由于 OP 发生了变异,因此如果它是纯的(当您优化回调时),则不会重新渲染 Item
  • 感谢您的回答。是的,我认为 return 属于某个地方的 else,但无论条件如何,它似乎总是会返回 todo。您的第二个回答对我这个新手来说很有意义,所以再次感谢您。
【解决方案3】:

您的地图函数总是返回一个项目。如果您正在修改的项目与当前正在映射的项目的 id 匹配,它只会先修改项目。 Map 返回一个新的项目数组(即使它没有改变任何东西),这会导致 useState 看到一个新值。默认情况下,在 React 中,检查更新不是很聪明——它只是检查 oldValue === newValue 是否。

对于像字符串这样的原语,只要两个单独的对象的值匹配,对象相等性测试就会返回 true。

"foo" === "foo"  // => true

但是,这不适用于对象或数组。包含相同值的两个不同数组比较相等(因为 Javascript 不比较它们的内容,而是比较它们的对象 ID):

["foo"] === ["foo"]  // => false

所以,当你map 你的items 时,你会得到一个新的数组对象(因为回想一下:map 将回调函数的返回值收集到一个新数组中),这永远不会 em> 匹配 items 的前一个值,所以每次调用 setItems 都会导致 React 说“嗯,我以前的 items 与我的新 items 不是同一个对象,我必须重新渲染这个组件”。

【讨论】:

  • 如果你优化它不会重新渲染(它会重新渲染列表而不是项目)。这是因为 OP 改变了具有匹配 id 的项目。使用items.map(item =&gt; item.id === id ? ((item.bought = !item.bought), item) : item )(来自OP 的变异代码)尝试我的答案中的代码。您可以 console.log 列表中的项目并查看它已更改,但该项目不会重新呈现,因为它是一个纯组件,并且它的 props.item 已发生变异,而不是重新引用。
猜你喜欢
  • 2019-08-06
  • 2020-04-29
  • 2020-11-23
  • 2020-08-20
  • 1970-01-01
  • 2020-01-31
  • 2020-02-05
  • 1970-01-01
  • 2019-10-17
相关资源
最近更新 更多