React函数组件使用useState更新状态后DOM未更新问题
我开发了一个展示channels数据并允许修改其minimum(最小值)和maximum(最大值)的React函数组件,channels数组示例如下:
[ { "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" } ]
组件通过props接收channels数组,将其拷贝到useState管理的channels状态中,渲染数据并提供输入框供用户修改值。用户输入时调用updateRanges更新channels状态,但输入框并未渲染新值,不符合预期。我加入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”的日志,请问这是什么原因?
问题原因
核心问题是直接修改了state数组的内部元素,然后传递同一个数组引用给setChannels。React的状态更新依赖于引用变化的浅比较:当你调用setChannels(channels)时,传入的数组和之前的state是同一个引用,React会认为状态没有发生变化,因此不会触发组件重渲染,自然也不会执行channels?.map里的代码,也就看不到对应的日志。
而firstName这类简单状态是直接替换了值(字符串是不可变类型),每次更新都是新的引用,所以能正常触发重渲染。
解决方案
更新状态时必须创建新的数组/对象副本,保证引用变化。修改updateRanges函数如下:
const updateRanges = (rowI, minOrMax, newRange) => { console.log("rowI, minOrMax, newRange is ", rowI, minOrMax, newRange); console.log("setting the channels..."); // 方式1:扩展运算符创建新数组和新对象 const newChannels = [...channels]; newChannels[rowI] = { ...newChannels[rowI], [minOrMax]: parseFloat(newRange) }; setChannels(newChannels); // 方式2:使用map生成新数组(更适合不可变更新风格) // const newChannels = channels.map((channel, idx) => { // if (idx === rowI) { // return { ...channel, [minOrMax]: parseFloat(newRange) }; // } // return channel; // }); // setChannels(newChannels); };
这样每次更新都会生成新的数组和对象引用,React检测到状态变化后会触发重渲染,输入框的值也会同步更新,同时能看到>>>looping through channels的日志。
内容的提问来源于stack exchange,提问作者Mark

