[英]Functional component not updating DOM when updating state with useState
编辑: Codepen 在这里https://codepen.io/mark-kelly-the-looper/pen/abGBwzv
我有一个显示“通道”并允许更改其最小值和最大值的组件。 通道数组的一个示例是:
[
{
"minimum": 0,
"maximum": 262144,
"name": "FSC-A",
"defaultScale": "lin"
},
{
"minimum": 0,
"maximum": 262144,
"name": "SSC-A",
"defaultScale": "lin"
},
{
"minimum": -27.719999313354492,
"maximum": 262144,
"name": "Comp-FITC-A - CTRL",
"defaultScale": "bi"
},
{
"minimum": -73.08000183105469,
"maximum": 262144,
"name": "Comp-PE-A - CTRL",
"defaultScale": "bi"
},
{
"minimum": -38.939998626708984,
"maximum": 262144,
"name": "Comp-APC-A - CD45",
"defaultScale": "bi"
},
{
"minimum": 0,
"maximum": 262144,
"name": "Time",
"defaultScale": "lin"
}
]
在我的组件中,我将channels
数组作为道具发送,将其复制到使用useState
的channels
object ,渲染通道数据,并允许用户使用input
更改值。 当他们输入时,我认为调用updateRanges
并简单地更新channels
。 然后我希望这个新值在input
中呈现。 奇怪的是,事实并非如此。
我包括了first_name
和last_name
作为测试,看看我是否在做一些根本错误的事情。 但是不,当我输入 first_name 和 last_name 时,新值会呈现在 UI 中。
我的组件是:
const [channels, setChannels] = useState([]);
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
useEffect(() => {
console.log("in Use Effect, setting the channels");
setChannels(JSON.parse(JSON.stringify(props.channels)));
}, [props.channels]);
const updateRanges = (rowI, minOrMax, newRange) => {
console.log("rowI, minOrMax, newRange is ", rowI, minOrMax, newRange);
console.log("setting the channels...");
channels[rowI]["minimum"] = parseFloat(newRange);
setChannels(channels);
};
return (
<div>
{channels?.map((rowData, rowI) => {
console.log(">>>looping through channels ");
return (
<div>
<input
style={{
//width: "20%",
outline: "none",
border: "none",
}}
value={rowData.minimum}
onChange={(newColumnData) => {
console.log("in the onChange ");
updateRanges(rowI, "minimum", newColumnData.target.value);
}}
/>
</div>
);
})}
<input
id="first_name"
name="first_name"
type="text"
onChange={(event) => setFirstName(event.target.value)}
value={firstName}
/>
<input
id="last_name"
name="last_name"
type="text"
value={lastName}
onChange={(event) => setLastName(event.target.value)}
/>
</div>
)
当我输入minimum
输入时(我在字段中输入 0 为 9),控制台记录:
in the onChange
rowI, minOrMax, newRange is 1 minimum 09
setting the channels...
我没有看到>>>looping through channels
可能会发生什么?
尝试这个。
useEffect(() => {
console.log("in Use Effect");
}, [channels]);
我刚刚想通了。 在你的 updateRanges 里面而不是
//comment this
//setChannels(channels);
用这个
// Replace with
setChannels([...channels]);
最终 function
const updateRanges = (rowI, minOrMax, newRange) => {
console.log("rowI, minOrMax, newRange is ", rowI, minOrMax, newRange);
channels[rowI][minOrMax] = parseFloat(newRange);
//comment this
//setChannels(channels);
// Replace with
setChannels([...channels]);
};
问题原因
每次将相同的 state 提供给 setState 时,React 都不会更新 DOM。 对于像 Number、String 和 Boolean 这样的原始值,很明显知道我们是否给出了不同的值。
另一方面,对于像 Object 和 Array 这样的引用值,更改它们的内容不会将它们标记为不同。 它应该是不同的 memory 参考
React 的基本规则之一是你不应该直接更新你的 state。 您应该将 state 视为只读的。 如果你修改了它,那么 React 就无法检测到你的 state 已经改变并正确地重新渲染你的组件/UI。
以下是您的代码面临的主要问题:
channels[rowI][minOrMax] = parseFloat(newRange);
setChannels(channels);
在这里,您将直接修改您的channels
state ,然后将修改后的 state 传递给setChannels()
。 在后台,React 将通过比较您传递给setChannels()
的新 state 是否与当前channels
state 不同来检查它是否需要重新渲染您的组件。 因为你直接修改了你的channels
state 并且正在通过你当前的 state 数组引用到setChannels()
钩子,所以 React 会看到你当前的 state 和你传递给钩子的值是相同的.
要正确更新您的 state,您应该使用类似.map()
的东西(如React 文档所建议的那样)来创建一个新数组,该数组具有自己的引用,该引用是唯一的并且不同于您当前的channels
state 引用。 在.map()
方法中,您可以访问当前项目索引来决定是否应该替换当前的 object,或者保持当前的 object 不变。 对于您要替换的 object,您可以将其替换为 object 的新实例,该实例的minOrMax
属性已更新如下:
const updateRanges = (rowI, minOrMax, newRange) => {
setChannels(channels => channels.map((channel, i) =>
i === rowI
? {...channel, [minOrMax]: Number(newRange)} // create a new object with "minumum" set to newRange if i === rowI
: channel // if i !== rowI then don't update the currennt item at this index
));
};
请注意,上面的.map()
返回一个新数组,它是 memory 中对channels
state 的完全不同的引用。 当我们想要更改数组内部的数据时,为它们创建新的引用也很重要,否则我们仍然会遇到UI 不更新的情况。 这就是为什么我们要创建一个新的 object
{...channel, [minOrMax]: Number(newRange)}
而不是直接修改 object
channel[minOrMax] = ...
在.map()
方法中。 您还经常会看到上述代码的变体,它执行以下操作:
const channelsCopy = JSON.parse(JSON.stringify(channels));
channelsCopy[rowI]["minimum"] = Number(newRange);
setChannels(channelsCopy);
或者可能是使用structuredClone
化克隆而不是.parse()
和.stringify()
的变体。 虽然这些确实有效,但它会克隆您的整个 state,这意味着不需要替换或更新的对象现在将被替换,无论它们是否更改,这可能会导致您的 UI 出现性能问题。
useImmer
的替代选项如您所见,与您最初拥有的相比,上面的内容几乎没有可读性。 幸运的是,有一个名为Immer的 package 可以让您更轻松地以更易读且与您最初编写的方式相似的方式编写不可变代码(如上所示)。 通过使用package,我们可以访问一个挂钩,使我们能够更轻松地更新我们的 state:
import { useImmer } from 'use-immer';
// ...
const [channels, updateChannels] = useImmer([]);
// ...
const updateRanges = (rowI, minOrMax, newRange) => {
updateChannels(draft => { // you can mutate draft as you wish :)
draft[rowI][minOrMax] = Number(newRange); // this is fine
});
}
const updateRanges = (rowI, minOrMax, newRange) => {
console.log("rowI, minOrMax, newRange is ", rowI, minOrMax, newRange);
console.log("setting the channels...");
channels[rowI]["minimum"] = parseFloat(newRange); // <-- MUTATING THE STATE!
setChannels(channels);
};
如您所见,您正在改变 state。 一旦你改变了 state 然后你调用setChannels(channels)
。 解决方案是在需要修改数据时创建数据副本。
现在想想下面的例子,
const [age, setAge] = useState(30);
const someEventHandler = () => setAge(30);
通过假设我们从未将age
更改为初始值30
以外的其他值,我们调用someEventHandler()
。 您可以看到setAge
将调用值30
,这与 state 中当前持有的值相同。
因此,为了提高性能,React 将检查是否使用当前 state 值调用了setState
。 如果是这种情况,那么 React 不会重新渲染你的组件。
由于 React 功能组件(应该)是纯的,那么设置相同的 state 应该与 output 相同的结果。 那么为什么要重新渲染呢?
您的代码也发生了同样的事情,您直接更新了channels
变量而不使用setState
。
channels[rowI]["minimum"] = parseFloat(newRange);
所以现在当你实际调用setChannel
时,React 会发现当前值和你要设置的值是相同的。 所以 React 不会做任何事情。
解决此问题的一种简单方法是在道具更改时使用与初始化通道 state 相同的技巧。
那是,
const updateRanges = (rowI, minOrMax, newRange) => {
const deepCpy = JSON.parse(JSON.stringify(channels))
deepCpy[rowI].minimum = parseFloat(newRange);
setChannels(deepCpy);
};
或者如果你使用lodash
库
const updateRanges = (rowI, minOrMax, newRange) => {
const deepCpy = cloneDeep(channels)
deepCpy[rowI].minimum = parseFloat(newRange);
setChannels(deepCpy);
};
您也可以通过阵列 map 进行更新,但如果channels
阵列大小发生变化并变大,我认为这不是实现的最佳解决方案。
并在 jsx 元素中添加<input type="number"...
以防止用户输入字符
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.